Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- UITextView
- uitableview
- HIG
- RxCocoa
- ios
- rxswift
- swift documentation
- 애니메이션
- Refactoring
- Observable
- MVVM
- 클린 코드
- swiftUI
- 리펙토링
- tableView
- map
- collectionview
- 리펙터링
- combine
- ribs
- Protocol
- SWIFT
- uiscrollview
- 리팩토링
- Human interface guide
- 스위프트
- Xcode
- Clean Code
- UICollectionView
- clean architecture
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 테이블 뷰 안의 수평 스크롤 뷰 (UITableView, UICollectionVIew, HorizontalScrollView) 본문
UI 컴포넌트 (swift)
[iOS - swift] 테이블 뷰 안의 수평 스크롤 뷰 (UITableView, UICollectionVIew, HorizontalScrollView)
jake-kim 2022. 3. 10. 23:36
구현 아이디어
- tableView의 커스텀 Cell에 collectionView를 넣어서 구현
- tableView의 커스텀 Cell은 collectionView를 가지고 있으므로, 커스텀 Cell에서 컬렉션 뷰에 뿌려줄 dataSource를 가지고 있는 상태
예제코드에서 사용한 프레임워크
- 코드로 UI 구현 시 편리함을 위해 사용
pod 'SnapKit'
pod 'Then'
샘플 Model 정의
- tableView와 collectionView에 표출될 샘플 모델 정의
- collectionViewCell에 사용될 데이터: Subcategory의 colors
- tableViewCell에 사용될 데이터: SubCategory의 name
- tableViewHeader에 사용될 데이터: CategoryModel의 name
struct CategoryModel {
let name: String
let subCategoryList: [SubCategory]
}
struct SubCategory {
let name: String
let colors: [ColorModel]
}
struct ColorModel {
let name: String
let color: UIColor
}
let sampleModel = [
CategoryModel(
name: "메인1",
subCategoryList: [
SubCategory(
name: "서브1.1",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브1.2",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
)
]
),
CategoryModel(
name: "메인2",
subCategoryList: [
SubCategory(
name: "서브2.1",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브2.2",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브123",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
)
]
),
CategoryModel(
name: "메인3",
subCategoryList: [
SubCategory(
name: "서브3.1",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
)
]
),
CategoryModel(
name: "메인4",
subCategoryList: [
SubCategory(
name: "서브4.1",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브4.2",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브4.3",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브4.4",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브4.5",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
)
]
),
CategoryModel(
name: "메인5",
subCategoryList: [
SubCategory(
name: "서브5.1",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브5.2",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
),
SubCategory(
name: "서브5.3",
colors: UIColor.getRandomColorList().map { ColorModel(name: "컬러(\($0.hexString)", color: $0) }
)
]
)
]
extension UIColor {
static var randomColor: UIColor {
UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0)
}
static func getRandomColorList() -> [UIColor] {
(0...((10...30).randomElement() ?? 10))
.map { _ in Self.randomColor }
}
var hexString: String {
let components = self.cgColor.components
let r = components?[0] ?? 0.0
let g = components?[1] ?? 0.0
let b = components?[2] ?? 0.0
return String(
format: "#%02lX%02lX%02lX",
lroundf(Float(r * 255)),
lroundf(Float(g * 255)),
lroundf(Float(b * 255))
)
}
}
- UI 상 가장 안쪽에 위치할 CollectionViewCell 정의
- 컬러 UIView
- 컬러 이름 UILabel
// ColorCollectionViewCell.swift
import UIKit
import SnapKit
import Then
final class ColorCollectionViewCell: UICollectionViewCell {
static let id = "ColorCollectionViewCell"
private let colorView = UIView()
private let nameLabel = UILabel().then {
$0.textColor = .black
$0.font = .systemFont(ofSize: 14)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.colorView)
self.addSubview(self.nameLabel)
self.colorView.snp.makeConstraints {
$0.left.top.right.equalToSuperview()
}
self.nameLabel.snp.makeConstraints {
$0.top.equalTo(self.colorView.snp.bottom)
$0.left.bottom.right.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError()
}
override func prepareForReuse() {
super.prepareForReuse()
self.prepare(color: nil, name: nil)
}
func prepare(color: UIColor?, name: String?) {
self.colorView.backgroundColor = color
self.nameLabel.text = name
}
}
- TableViewCell 정의
- 서브1.1 - UILabel
- 수평 스크롤 뷰 - UICollectionView
- collectionView에 뿌려줄 dataSource도 준비
// CategoryTableViewCell.swift
import UIKit
import SnapKit
import Then
final class CategoryTableViewCell: UITableViewCell {
static let id = "CategoryTableViewCell"
private let titleLabel = UILabel().then {
$0.textColor = .black
$0.font = .systemFont(ofSize: 15)
}
private lazy var collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout().then {
$0.scrollDirection = .horizontal
$0.minimumLineSpacing = 2.0
$0.minimumInteritemSpacing = 5.0
$0.itemSize = CGSize(width: 150, height: 180)
}
).then {
$0.showsHorizontalScrollIndicator = false
$0.contentInset = .init(top: 10, left: 10, bottom: 10, right: 10)
$0.backgroundColor = .clear
$0.dataSource = self
$0.register(ColorCollectionViewCell.self, forCellWithReuseIdentifier: ColorCollectionViewCell.id)
}
private let bottomPaddingView = UIView().then {
$0.backgroundColor = .white
}
private var colorModelList = [ColorModel]()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .systemGray4
self.contentView.addSubview(self.titleLabel)
self.contentView.addSubview(self.collectionView)
self.contentView.addSubview(self.bottomPaddingView)
self.titleLabel.snp.makeConstraints {
$0.top.equalToSuperview()
$0.left.equalToSuperview().inset(10)
}
self.collectionView.snp.makeConstraints {
$0.top.equalTo(self.titleLabel.snp.bottom)
$0.left.right.equalToSuperview()
}
self.bottomPaddingView.snp.makeConstraints {
$0.left.right.bottom.equalToSuperview()
$0.top.equalTo(self.collectionView.snp.bottom)
$0.height.equalTo(10).priority(999)
}
}
required init?(coder: NSCoder) {
fatalError()
}
override func prepareForReuse() {
super.prepareForReuse()
self.prepare(subTitle: nil, colorModelList: [])
}
func prepare(subTitle: String?, colorModelList: [ColorModel]) {
self.titleLabel.text = subTitle
self.colorModelList = colorModelList
self.collectionView.reloadData()
}
}
extension CategoryTableViewCell: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
self.colorModelList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ColorCollectionViewCell.id, for: indexPath) as! ColorCollectionViewCell
let colorModel = self.colorModelList[indexPath.item]
cell.prepare(color: colorModel.color, name: colorModel.name)
return cell
}
}
- TableViewHeader 정의
- 메인1 - UILabel
// MainCategoryHeaderView.swift
import UIKit
import SnapKit
import Then
final class MainCategoryHeaderView: UITableViewHeaderFooterView {
static let id = "MainCategoryHeaderView"
private let titleLabel = UILabel().then {
$0.textColor = .black
$0.numberOfLines = 0
$0.font = .systemFont(ofSize: 22, weight: .bold)
}
private let bottomPaddingView = UIView()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
self.addSubview(self.titleLabel)
self.addSubview(self.bottomPaddingView)
self.titleLabel.snp.makeConstraints {
$0.left.top.right.equalToSuperview()
}
self.bottomPaddingView.snp.makeConstraints {
$0.left.right.bottom.equalToSuperview()
$0.top.equalTo(self.titleLabel.snp.bottom)
$0.height.equalTo(10).priority(999)
}
}
required init?(coder: NSCoder) {
fatalError()
}
override func prepareForReuse() {
super.prepareForReuse()
self.prepare(title: nil)
}
func prepare(title: String?) {
self.titleLabel.text = title
}
}
- 사용하는쪽 - ViewController
- dataSource를 tableView에도 맵핑하는 동시에, cellForRowAt에서도 각 cell에 collectionView에 뿌려줄 데이터도 넘기는 형태
// ViewController.swift
import UIKit
import SnapKit
import Then
class ViewController: UIViewController {
private lazy var tableView = UITableView().then {
$0.separatorStyle = .none
$0.rowHeight = 220
$0.dataSource = self
$0.delegate = self
$0.register(CategoryTableViewCell.self, forCellReuseIdentifier: CategoryTableViewCell.id)
$0.register(MainCategoryHeaderView.self, forHeaderFooterViewReuseIdentifier: MainCategoryHeaderView.id)
}
private var dataSource = [CategoryModel]()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.tableView)
self.tableView.snp.makeConstraints {
$0.edges.equalTo(self.view.safeAreaLayoutGuide)
}
self.dataSource = sampleModel
self.tableView.reloadData()
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
self.dataSource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.dataSource[section].subCategoryList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CategoryTableViewCell.id, for: indexPath) as! CategoryTableViewCell
let row = self.dataSource[indexPath.section].subCategoryList[indexPath.row]
cell.prepare(subTitle: row.name, colorModelList: row.colors)
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableHeaderFooterView(withIdentifier: MainCategoryHeaderView.id) as! MainCategoryHeaderView
cell.prepare(title: self.dataSource[section].name)
return cell
}
}
* 전체 코드: https://github.com/JK0369/ExTableCeollectionView
* 참고
https://johncodeos.com/how-to-add-uicollectionview-inside-uitableviewcell-using-swift/
'UI 컴포넌트 (swift)' 카테고리의 다른 글
Comments