์ด๋ชจ์ ๋ชจ/UIKit
Modern UIKit Collection, TableView #2-3
ARpple
2023. 9. 3. 17:52
Advances in Collection View Layout #3
Advances in Collection View Layout - WWDC19 - Videos - Apple Developer
Collection View Layouts make it easy to build rich interactive collections. Learn how to make dynamic and responsive layouts that range...
developer.apple.com
๐ก Collection View Layout์ ์ฌ์ฉํ๋ฉด ํ๋ถํ interactive ์ปฌ๋ ์ ์ ์ฝ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค. ๊ธฐ๋ณธ ๋ชฉ๋ก๋ถํฐ ๋ค์ฐจ์ ํ์ ํ๊ฒฝ๊น์ง ๋์ ์ด๊ณ ๋ฐ์์ด ๋น ๋ฅธ ๋ ์ด์์์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์.
CompositionalLayout ์ ์ฉํ๊ธฐ
โ UICell๋ก ์์ํ๋ ํค์๋๋ WWDC 2020 ์ดํ์ ๋์จ ๋ด์ฉ
SectionProvider ๋ง๋ค๊ธฐ
// ๊ทธ๋ฅ ์น์
์ ๊ตฌ๋ถํ๊ธฐ ์ํ Enum
enum Account.Section:Int, Hashable, CaseIterable{
case header,main,footer
}
let sectionProvider = { (sectionIndex:Int, layoutEnvorionment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = AccountVC.Section(rawValue: sectionIndex) else {return nil}
let section: NSCollectionLayoutSection
switch sectionKind{
case .header:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.33))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,repeatingSubitem: item, count: 1)
section = NSCollectionLayoutSection(group: group)
case .footer,.main:
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.headerMode = .supplementary
section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvorionment)
// section.interGroupSpacing = 8
}
return section
}
Layout ๊ตฌ์ฑํ๊ณ CollectionView ๋ง๋ค๊ธฐ
var config = UICollectionViewCompositionalLayoutConfiguration()
// config.scrollDirection = .horizontal
// CollectionView ์์ฒด์ Header ๋ฃ๊ธฐ
var boundaryItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44)), elementKind: "LayoutHeader", alignment: .top)
config.boundarySupplementaryItems = [boundaryItem]
let layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider,configuration: config)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
DiffableDataSource์ Cell ์ ์ฉํ๊ธฐ
SupplementaryView
1. CustomSupplymentView ๋ง๋ค๊ธฐ
class TempReusableView: UICollectionReusableView{
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("isError")
}
}
2. SupplementaryItem ๋ฑ๋ก์ฉ ์ธ์คํด์ค ๋ง๋ค๊ธฐ
let layoutHeaderRegistration = UICollectionView.SupplementaryRegistration<TempReusableView>(elementKind: "LayoutHeader") { supplementaryView, elementKind, indexPath in
supplementaryView.backgroundColor = .blue
}
let sectionHeaderRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) {
[weak self] headerView, elementKind, indexPath in
let headerItem = self?.diffableDataSource?.snapshot().sectionIdentifiers[indexPath.section]
var config = headerView.defaultContentConfiguration()
config.text = headerItem?.description
if headerItem != Section.header{ headerView.contentConfiguration = config }
}
3. DiffableDataSource์์ ์ฌ์ฉํ๋๋ก ๋ฑ๋กํ๊ธฐ
self.diffableDataSource?.supplementaryViewProvider = { collectionView,elementKind,indexPath -> UICollectionReusableView? in
switch elementKind{
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderRegistration, for: indexPath)
default:
return collectionView.dequeueConfiguredReusableSupplementary(using: layoutHeaderRegistration, for: indexPath)
}
}
CompositionalCell
1. CustomCell ๋ง๋ค๊ธฐ
class HeaderCell: UICollectionViewCell{
var label = UILabel()
var textField = UITextField()
lazy var profileImageView = {
let btn = UIButton()
var config = UIButton.Configuration.plain()
// uiimage ์์ฒด์ ์์ ์ค์ ํ๊ธฐ
let image = UIImage(systemName: "person.circle")?.withTintColor(.white, renderingMode: .alwaysOriginal)
config.background.image = AppManager.shared.accountImage ?? image
config.background.image?.withTintColor(.darkGray)
config.background.backgroundColor = .systemGray5
config.background.imageContentMode = .scaleAspectFill
btn.configuration = config
return btn
}()
let avaterImageView = {
let btn = UIButton()
var config = UIButton.Configuration.plain()
// uiimage ์์ฒด์ ์์ ์ค์ ํ๊ธฐ
let image = UIImage(systemName: "face.smiling")?.withTintColor(.white, renderingMode: .alwaysOriginal)
config.background.image = image
config.background.image?.withTintColor(.darkGray)
config.background.backgroundColor = .systemGray5
config.background.imageContentMode = .scaleAspectFill
btn.configuration = config
return btn
}()
let info = {
let v = UIButton()
var config = UIButton.Configuration.gray()
config.baseBackgroundColor = .clear
config.attributedTitle = .init("์ฌ์ง ๋๋ ์๋ฐํ ์์ ",attributes: .init([
NSAttributedString.Key.font : UIFont.preferredFont(forTextStyle: .subheadline)
]))
config.contentInsets = .zero
v.configuration = config
return v
}()
private lazy var stView = {
let stView = UIStackView(arrangedSubviews: [profileImageView,avaterImageView])
stView.axis = .horizontal
stView.distribution = .fillEqually
stView.spacing = 16
stView.alignment = .fill
return stView
}()
override init(frame: CGRect) {
super.init(frame: .zero)
viewConfiguration()
setConstraints()
}
required init?(coder: NSCoder) {
fatalError("this cell is error")
}
private func viewConfiguration(){
[stView,info].forEach{contentView.addSubview($0)}
}
func setConstraints(){
stView.snp.makeConstraints { make in
make.top.centerX.equalToSuperview()
make.bottom.equalTo(info.snp.top).offset(-8)
// make.bottom.equalToSuperview()
}
[profileImageView,avaterImageView].forEach{ v in
v.snp.makeConstraints { make in
make.width.equalTo(v.snp.height)
}
}
info.snp.makeConstraints { make in
make.top.equalTo(stView.snp.bottom)
// .offset(8)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview()
}
info.setContentHuggingPriority(.init(253), for: .vertical)
// info.setContentCompressionResistancePriority(.init(752), for: .vertical)
}
func dequeueCompletion(){
DispatchQueue.main.async {
self.profileImageView.configuration?.background.cornerRadius = self.profileImageView.frame.width / 2
self.avaterImageView.configuration?.background.cornerRadius = self.avaterImageView.frame.width / 2
}
}
}
2. Cell ๋ฑ๋ก์ฉ ์ธ์คํด์ค ๋ง๋ค๊ธฐ
// ์
์์ ์ฌ์ฉํ๊ธฐ ์ํ ์ ๋ณด๋ฅผ ๋ด์ Item
struct Item:Hashable,Identifiable{
internal var id:String{ keyInfo }
// let keyInfo: Account
let keyInfo: String
var label: String
var placeholder: String?
var input:String?
}
let mainRegistration = UICollectionView.CellRegistration<UICollectionViewListCell,Item> {cell, indexPath, itemIdentifier in
var defaultConfig = cell.defaultContentConfiguration()
defaultConfig.text = itemIdentifier.label
defaultConfig.prefersSideBySideTextAndSecondaryText = true
let backConfig = UIBackgroundConfiguration.listPlainCell()
cell.backgroundConfiguration = backConfig
cell.contentConfiguration = defaultConfig
cell.accessories = [.label(text: itemIdentifier.placeholder ?? ""),.disclosureIndicator()]
}
let headerRegistration = UICollectionView.CellRegistration<AccountView.HeaderCell,Item> { cell, indexPath, itemIdentifier in
cell.dequeueCompletion()
}
let footerRegistration = UICollectionView.CellRegistration<UICollectionViewListCell,Item> {cell, indexPath, itemIdentifier in
var defaultConfig = cell.defaultContentConfiguration()
defaultConfig.attributedText = NSAttributedString(string: itemIdentifier.label,attributes: [
NSAttributedString.Key.foregroundColor: itemIdentifier.keyInfo == "privacySettiing" ?
UIColor.tintColor : UIColor.label
])
cell.contentConfiguration = defaultConfig
}
3. DiffableDataSource์์ ์ฌ์ฉํ๋๋ก ๋ฑ๋กํ๊ธฐ
self.diffableDataSource = UICollectionViewDiffableDataSource<Section,Item>(collectionView: mainView.collectionView){[weak self] c, indexPath, item in
guard let self else {fatalError("weak self error \(#function)")}
guard let section:AccountVC.Section = AccountVC.Section(rawValue: indexPath.section) else { fatalError("์๋ ์น์
") }
// ์ฌ๊ธฐ ๋ฐ๋ณต์ ์ด๋ป๊ฒ ์ค์ผ๊น...
switch section{
case .footer:
return c.dequeueConfiguredReusableCell(using: footerRegistration, for: indexPath, item: item)
case .header:
let cell = c.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: item)
self.$profileImage.sink { image in cell.profileImageView.configuration?.background.image = image }
.store(in: &self.subscription)
cell.profileImageView.addAction(.init(handler: {[weak self] _ in self?.present(self!.photoPicker,animated: true) }),
for: .touchUpInside)
cell.info.addAction(.init(handler: {[weak self] _ in
self?.present(self!.infoAlert,animated: true) }), for: .touchUpInside)
return cell
case .main:
return c.dequeueConfiguredReusableCell(using: mainRegistration, for: indexPath, item: item)
}
}