์ด๋ชจ์ €๋ชจ/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)
            }
        }