Swift - ~= 연산자란?

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


~= 연산자

대상이 특정 범위에 속하는 지 범위를 체크하는 연산자
switch 구문에 ~= 연산자가 사용되는데, case의 범위를 확인할 때 내부적으로 ~=연산자를 불러 사용함

~= 연산자는 비교연산자 == 의 확장된 상태로 생각할 수 있다.

두 대상이 같은 지 단순 비교할 때 == 연산자를 사용하는데 이와 달리 ~=는 대상이 어떤 범위에 속하는지를 확인!

swift - ==와 ===

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


==

a == b: a값과 b값이 같은 지 비교

let v1 = 1
let v2 = 2

print(v1 == v2) // false

===

a === b: a가 참조하고 있는 인스턴스와 b가 참조하고 있는 인스턴스가 같은 지 비교

swift에서는 크게 값, 참조타입이 존재하는데 참조타입을 가질 수 있는 클래스 객체를 참조할 수 있는 변수는 다수가 될 수 있기 때문에 === 연산자가 필요하다.

let c1 = Person(id: 1, name: "zehye")
let c2 = Person(id: 1, name: "jihye")
let c3 = c1

print(p1 === p2)  // false
print(p1 === p3)  // true

클래스의 인스턴스는 heap 영역에 할당되며 인스턴스를 참조하는 변수는 stack영역에 할당된다.
그래서 ===는 heap 영역의 값을 비교하고 ==는 stack영역의 값을 비교한다.

Swift - Mutating이란?

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


Mutating

구조체의 메서드가 구조체 내부에서 데이터 수정될때는 Mutating 키워드를 선언해야함

struct Point {
  var x = 0
  var y = 0

  mutating func moveTo(x: Int, y: Int) {
    self.x = x
    self.y = y
  }
}

즉, 다른 구조체안에서 Mutating이 있느냐 없으냐에 따라 원래 구조체 내부의 값을 변경하는 api인지 아닌지 유추가 가능하다.

Advances CollectionvView - Custom Cell 로 구성해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


개요

일반적으로 컬렉션뷰는 dataSource를 통해 셀에 표시할 정보를 가져옵니다.

뷰 컨트롤러에서 dataSource 프로토콜을 채택하게되면 기본적으로 실행해야할 메소드들이 있고, 이를 통해 우리는 셀에 대한 데이터를 가져와 화면에 보여줄 수 있게 됩니다. 그런데 WWDC19부터 이번에 발표했던 WWDC21까지 dataSource를 넘어서 cell에 대한 새로운 기능, 더 확장되고 심화된 개념의 컬렉션뷰가 등장하였습니다.

애플에서도 이를 Advances CollectionView라 칭하면서 그 안에 다양한 기능들을 소개하고 있습니다.

이번 포스팅에서 저는 이 Advances CollectionView에서 소개하는 크게 총 2가지의 기능을 사용해 프로젝트를 구성해보려고 합니다.

  1. Diffable DataSource / Section Snapshot
  2. UICollectionViewCompositionalLayout

만들어볼 화면 구성은 다음과 같습니다.

  1. CollectionView를 통해 json 파일 내 데이터를 보여준다.
  2. CollectionView 내 데이터 구성은 총 3개의 Section으로 나누어 보여준다.
  3. 해당 데이터는 DiffableDataSource를 통해 Cell에 나타내도록 한다.
  4. 각 셀은 커스텀 셀로 구성되어진다.
  • 추가적으로 확인해 볼 내용은 다음과 같습니다.
  1. 셀이 무한 스크롤 되는 경우 dataSource와 layout이 어떻게 호출되는 지 확인
  2. 섹션 사이에 다른 섹션이 들어갈 수 있는지 확인

1~4까지는 기본적으로 만들어볼 화면이며, 추가 1,2는 글 하단에 추가적으로 다뤄볼 예정입니다.


Advances CollectionView - CustomCell로 구성해보기

우선 프로젝트에 들어가기 앞서 저는 json 파일을 제 프로젝트 내에 넣어놓고 해당 파일을 불러와 데이터를 가져올 것입니다.

json 파일은 아래와 같습니다. (Home.json)

{
    "companyInfoList" : [
        {
            "companyName" : "병원",
            "addrRoad" : "서울 특별시",
            "introPath" : ""
        },
    ],
    "expertList" : [
        {
            "expertInfo" : {
                "expertTypeName" : "한의사",
                "name" : "박지혜",
                "profilePath": ""
            }
        }
    ],
    "consultList" : [
        {
            "regDate" : 1635472394000,
            "readCnt" : 22,
            "title" : "감기에 걸렸을때 어느 병원으로 가야할까요?",
            "lastAnswer" : {
                "content" : "안녕하세요. 박지혜입니다.\n",
                "profileImg" : "",
            }
        }
    ]
}

해당 데이터를 불러오기 위해 모델을 구성해보겠습니다.
json 파일 데이터를 불러오기 위해 swiftyJSON을 사용하였습니다.


Model Company

import UIKit
import SwiftyJSON

struct Company {
    var addrRoad: String?
    var companyName: String?
    var introPath: UIImage? = nil

    init(_ json: [String: JSON]?) {
        let json = json ?? [:]
        self.addrRoad = json["addrRoad"]?.string ?? ""
        self.companyName = json["companyName"]?.string ?? ""
        if let url = URL(string: json["introPath"]?.stringValue ?? ""),
           let image = try? Data(contentsOf: url) {
            self.introPath = UIImage(data: image)
        }
    }

    init(_ json: JSON?) {
        self.init(json?.dictionaryValue)
    }
}

extension Company: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(addrRoad)
        hasher.combine(companyName)
        hasher.combine(introPath)
    }
}


Model Expert

import UIKit
import SwiftyJSON

struct Expert {
    var expertTypeName: String?
    var name: String?
    var profilePath: UIImage? = nil

    init(_ json: [String:JSON]?) {
        let json = json ?? [:]
        self.expertTypeName = json["expertInfo"]?["expertTypeName"].string ?? ""
        self.name = json["expertInfo"]?["name"].string ?? ""
        if let url = URL(string: json["expertInfo"]?["profilePath"].stringValue ?? ""),
           let image = try? Data(contentsOf: url) {
            self.profilePath = UIImage(data: image)
        }
    }

    init(_ json: JSON?) {
        self.init(json?.dictionaryValue)
    }
}

extension Expert: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(expertTypeName)
        hasher.combine(name)
        hasher.combine(profilePath)
    }
}


Model Consult

import UIKit
import SwiftyJSON

struct Consult {
    let title: String?
    let readCnt: Int?
    let content: String?
    var regDate: Date?
    var profileImg: UIImage? = nil

    init(_ json: [String:JSON]?) {
        let json = json ?? [:]
        self.title = json["title"]?.string ?? ""
        self.readCnt = json["readCnt"]?.int ?? 0
        self.content = json["lastAnswer"]?["content"].string ?? ""
        if let url = URL(string: json["lastAnswer"]?["profileImg"].stringValue ?? ""),
           let image = try? Data(contentsOf: url) {
            self.profileImg = UIImage(data: image)
        }
        self.regDate = Date(timeIntervalSince1970: (json["regDate"]!.doubleValue/1000))
    }

    init(_ json: JSON?) {
        self.init(json?.dictionaryValue)
    }
}

extension Consult: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(title)
        hasher.combine(readCnt)
        hasher.combine(content)
        hasher.combine(profileImg)
        hasher.combine(regDate)
    }
}

이렇게 모델을 구성하고 나면 실제 json 파일을 해당 모델에 맞게 불어와야겠죠.


ViewController

import UIKit
import SwiftyJSON

class ViewController: UIViewController {
    var companyList = Array<Company>()
    var consultList = Array<Consult>()
    var expertList = Array<Expert>()

    func setData() {
        if let path = Bundle.main.path(forResource: "Home", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { print("path : \(path)")
            do {
                let jsonResult = try JSON(data: data)

                let companyResult = jsonResult["companyInfoList"]
                self.companyList = companyResult.arrayValue.compactMap({Company($0)})

                let consultResult = jsonResult["consultList"]
                self.consultList = consultResult.arrayValue.compactMap({Consult($0)})

                let expertResult = jsonResult["expertList"]
                self.expertList = expertResult.arrayValue.compactMap({Expert($0)})

            }catch {
                print("error : \(error)")
            }
        }
    }
}

이렇게 하면 프로젝트 내 있는 jons파일을 불러와 제가 원하는 데이터를 가지고 있을 수 있게 됩니다.
이때 각 모델에서 Hashable 프로토콜을 채택한 이유는 무엇일까요?

우리는 앞으로 Diffable DataSource를 사용할 것입니다.

이때 해당 데이터들은 반드시 각각 본인이 고유한 데이터임을 증명해주어야 합니다. 그렇기 때문에 이렇게 Hashable을 통해 진행을 하게 되는데요.
사실상 더 많은 데이터를 다루기에는 위 같은 방법은 옳지 않습니다.

프로젝트 진행시 넘어오는 json 안에 seq 혹은 uid 같이 반드시 고유할것이라는 증명된 변수가 있다면
이 변수를 고유하게 지정해주면 됩니다.


Section과 Item

저는 우선 화면 구성을 크게 3개의 section으로 구분지어 표현할 예정입니다.

  1. Consult Section
  2. Company Section
  3. Expert Section

그리고 각 섹션에는 Json 파일로부터 긁어온 데이터들을 아이템으로 넣어줄 것입니다.
이것을 코드로 표현해보겠습니다.


class ViewController: UIViewController {

    enum Section: Int, Hashable, CaseIterable, CustomStringConvertible {
        case consult, company, expert

        var description: String {
            switch self {
            case .consult: return "Consult"
            case .company: return "Company"
            case .expert: return "Expert"
            }
        }
    }

    struct Item: Hashable {
        let expert: Expert?
        let consult: Consult?
        let company: Company?

        init(consult: Consult? = nil, company: Company? = nil, expert: Expert? = nil) {
            self.expert = expert
            self.consult = consult
            self.company = company
        }

        private let identifier = UUID()
    }
}


Section과 Item들의 레이아웃 설정

각 섹션과 아이템들의 위치 즉 레이아웃을 잡아주기 위해 우리는 UICollectionViewCompositionalLayout을 사용할 것입니다.

이 개념을 이해하기 위한 사진 하나를 보여드리겠습니다.

해당 사진에서 유추할 수 있듯 각 레이아웃을 잡아주는 순서는 다음과 같습니다.

  1. item에서 group
  2. group에서 section
  3. 이렇게 결합한 뒤 전체 layout으로 구성요소 결합

각 섹션에 대한 코드는 다음과 같습니다.


func createLayout() -> UICollectionViewLayout {
    print("createLayout")
    let sectionProvider = { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
        guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }
        let section: NSCollectionLayoutSection
        if sectionKind == .consult{
            print("layout consult")
            // item의 width와 height 지정
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)

            let groupHeight = NSCollectionLayoutDimension.absolute(200)
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: groupHeight)
            // 그룹 내 1.0 배율 안에서 보여질 아이템의 수는 1개라는 의미
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

            section = NSCollectionLayoutSection(group: group)

        } else if sectionKind == .company {
            print("layout company")
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)

            let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(350), heightDimension: .absolute(200))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

            section = NSCollectionLayoutSection(group: group)
            section.interGroupSpacing = 10
            section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
            section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)

        } else if sectionKind == .expert {
            print("layout expert")
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)

            let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(150), heightDimension: .absolute(150))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

            section = NSCollectionLayoutSection(group: group)
            section.interGroupSpacing = 10
            section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
            section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        }
        else {
            fatalError("Unknown section!")
        }
        return section
    }
    return UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)
}

코드를 살펴보면 Consult Section의 경우 수직으로 스크롤이 되고 있으며, 나머지 Compant, Expert Section은 수평으로 스크롤이 되고있음을 알 수 있습니다.

이를 구분짓는 코드는 section.orthogonalScrollingBehavior입니다.
하나의 섹션 단위의 스크롤 방향을 지정해주는 것으로 이 행위를 넣지 않으면 디폴트는 수직입니다.
그래서 데이터들이 아래로 쌓여져 보이게 될 것입니다.

추가로 코드 안에서 NSCollectionLayoutSize를 통해 item과 group의 사이즈(width, height)를 구성하고 있음을 볼 수 있습니다.

  • absolute: 절대값, 정확한 치수를 지정할 때 사용
  • estimated: 런타임에 컨텐츠의 크기가 변경될 수 있는 경우 예상값을 사용
  • fractional: 분구삾을 통해 item 컨테이너의 너비 기준으로 상대적인 값 정의


Custom Cell

저는 커스텀셀을 만들어와 커스텀 셀을 연결해줄 예정입니다.

커스텀 셀은 아래와 같이 정의하였습니다.

import UIKit

class CompanyCollectionViewCell: UICollectionViewCell {
    private let profielImgView: UIImageView = {
        let imgView = UIImageView()
        imgView.contentMode = .scaleAspectFill
        imgView.clipsToBounds = true
        imgView.backgroundColor = .clear
        return imgView
    }()

    private let companyLbl: UILabel = {
        let lbl = UILabel()
        lbl.font = .systemFont(ofSize: 16)
        lbl.textAlignment = .center
        return lbl
    }()

    private let addressLbl: UILabel = {
        let lbl = UILabel()
        lbl.font = .systemFont(ofSize: 14)
        lbl.textAlignment = .center
        return lbl
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.initView()
    }

    required init?(coder: NSCoder) {
        fatalError("init")
    }

    func initView() {
        self.contentView.backgroundColor = .clear

        self.contentView.addSubview(profielImgView)
        self.contentView.addSubview(companyLbl)
        self.contentView.addSubview(addressLbl)

        self.profielImgView.snp.makeConstraints {(make) in
            make.trailing.equalToSuperview().inset(12)
            make.leading.equalToSuperview()
            make.height.equalTo(140)
            make.top.equalToSuperview()
        }

        self.companyLbl.snp.makeConstraints{(make) in
            make.top.equalTo(self.profielImgView.snp.bottom).inset(-8)
            make.trailing.leading.equalToSuperview().inset(24)
        }

        self.addressLbl.snp.makeConstraints {(make) in
            make.top.equalTo(self.companyLbl.snp.bottom).inset(-8)
            make.leading.trailing.equalTo(self.companyLbl)
            make.bottom.lessThanOrEqualToSuperview().inset(8)
        }
    }

    func configure(_ data: Company) {
        self.profielImgView.image = data.introPath
        self.companyLbl.text = data.companyName
        self.addressLbl.text = data.addrRoad
    }
}

Cell 파일에서는 단순히 셀에 들어가는 요소들의 객체를 만들어주고 각각의 위치를 잡아주고 있습니다.
특징이라면 configure 메소드에서 모델의 데이터들을 각각 연결해주고 있다는 것이죠.

이렇게 만들어놓은 셀 파일을 이제 뷰컨트롤러에서 등록해주고 연결할 것입니다.


func createCell() {
    print("createCell")
    self.collectionView.register(CompanyCollectionViewCell.self, forCellWithReuseIdentifier: "CompanyCell")
    self.collectionView.register(ConsultCollectionViewCell.self, forCellWithReuseIdentifier: "ConsultCell")
    self.collectionView.register(ExpertCollectionViewCell.self, forCellWithReuseIdentifier: "ExpertCell")
}

func createDataSource() {
    print("createDataSource")
        self.dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: self.collectionView) {(collectionView, indexPath, item) -> UICollectionViewCell? in
        guard let section = Section(rawValue: indexPath.section) else { fatalError() }
        switch section {
        case .company:
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CompanyCell", for: indexPath) as? CompanyCollectionViewCell else { preconditionFailure() }
            cell.configure(item.company!)
            return cell
        case .consult:
            print("datasource consult")
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConsultCell", for: indexPath) as? ConsultCollectionViewCell else { preconditionFailure() }
            cell.configure(item.consult!)
            return cell
        case .expert:
            print("datasource expert")
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ExpertCell", for: indexPath) as? ExpertCollectionViewCell else { preconditionFailure() }
            cell.configure(item.expert!)
            return cell
        }
    }
}

위 코드는 우리가 일반적으로 컬렉션뷰 데이터소스 프로토콜을 채택했을 때 사용하는 CellForItem 메소드 내 구성과 비슷한것을 볼수 있죠?


Snapshot

이렇게 구성한 데이터소소를 가지고 이제 snapshot을 구성해보도록 하겠습니다.

func applySnapshot() {
    print("appleSnapshot")
    let sections = Section.allCases
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
    snapshot.appendSections(sections)
    print(sections)
    print(snapshot)

    let consultItems = self.consultLis.map { Item(consult: $0) }
    var consultSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
    consultSnapshot.append(consultItems)

    let companyItems = self.companyList.map { Item(company: $0) }
    companySnapshot.append(companyItems)


    let expertItems = self.expertList.map { Item(expert: $0) }
    var expertSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
    expertSnapshot.append(expertItems)

    dataSource.apply(consultSnapshot, to: .consult, animatingDifferences: false)
    dataSource.apply(companySnapshot, to: .company, animatingDifferences: false)
    dataSource.apply(expertSnapshot, to: .expert, animatingDifferences: false)
}

이렇게 적용을 하면 제가 원하는 대로 실행이 될 것입니다.


아래는 전체 소스코드 입니다.

import UIKit
import SwiftyJSON

class ViewController: UIViewController {

    enum Section: Int, Hashable, CaseIterable, CustomStringConvertible {
        case consult, company, expert

        var description: String {
            switch self {
            case .consult: return "Consult"
            case .company: return "Company"
            case .expert: return "Expert"
            }
        }
    }

    struct Item: Hashable {
        let expert: Expert?
        let consult: Consult?
        let company: Company?

        init(consult: Consult? = nil, company: Company? = nil, expert: Expert? = nil) {
            self.expert = expert
            self.consult = consult
            self.company = company
        }

        private let identifier = UUID()
    }

    var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    @IBOutlet weak var collectionView: UICollectionView!
    var companyList = Array<Company>()
    var consultList = Array<Consult>()
    var expertList = Array<Expert>()

    override func viewDidLoad() {
        overrideUserInterfaceStyle = .light
        super.viewDidLoad()

        setData()
        setNavigation()
        createDataSource()
        self.collectionView.collectionViewLayout = self.createLayout()
        createCell()
        applySnapshot()
    }

    func setData() {
        print("setData")
        if let path = Bundle.main.path(forResource: "Home", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { print("path : \(path)")
            do {
                let jsonResult = try JSON(data: data)

                let companyResult = jsonResult["companyInfoList"]
                self.companyList = companyResult.arrayValue.compactMap({Company($0)})

                let consultResult = jsonResult["consultList"]
                self.consultList = consultResult.arrayValue.compactMap({Consult($0)})

                let expertResult = jsonResult["expertList"]
                self.expertList = expertResult.arrayValue.compactMap({Expert($0)})

            }catch {
                print("error : \(error)")
            }
        }
    }

    func createLayout() -> UICollectionViewLayout {
        print("createLayout")
        let sectionProvider = { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }
            let section: NSCollectionLayoutSection
            if sectionKind == .consult{
                print("layout consult")
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)

                let groupHeight = NSCollectionLayoutDimension.absolute(200)
                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: groupHeight)
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

                section = NSCollectionLayoutSection(group: group)

            } else if sectionKind == .company {
                print("layout company")
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)

                let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(350), heightDimension: .absolute(200))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

                section = NSCollectionLayoutSection(group: group)
                section.interGroupSpacing = 10
                section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)

            } else if sectionKind == .expert {
                print("layout expert")
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)

                let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(150), heightDimension: .absolute(150))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

                section = NSCollectionLayoutSection(group: group)
                section.interGroupSpacing = 10
                section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
            }
            else {
                fatalError("Unknown section!")
            }
            return section
        }
        return UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)
    }

    func createCell() {
        print("createCell")
        self.collectionView.register(CompanyCollectionViewCell.self, forCellWithReuseIdentifier: "CompanyCell")
        self.collectionView.register(ConsultCollectionViewCell.self, forCellWithReuseIdentifier: "ConsultCell")
        self.collectionView.register(ExpertCollectionViewCell.self, forCellWithReuseIdentifier: "ExpertCell")
    }

    func createDataSource() {
        print("createDataSource")
            self.dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: self.collectionView) {(collectionView, indexPath, item) -> UICollectionViewCell? in
            guard let section = Section(rawValue: indexPath.section) else { fatalError() }
            switch section {
            case .company:
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CompanyCell", for: indexPath) as? CompanyCollectionViewCell else { preconditionFailure() }
                cell.configure(item.company!)
                return cell
            case .consult1, .consult2:
                print("datasource consult")
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConsultCell", for: indexPath) as? ConsultCollectionViewCell else { preconditionFailure() }
                cell.configure(item.consult!)
                return cell
            case .expert:
                print("datasource expert")
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ExpertCell", for: indexPath) as? ExpertCollectionViewCell else { preconditionFailure() }
                cell.configure(item.expert!)
                return cell
            }
        }
    }

    func setNavigation() {
        navigationItem.title = "홈"    
    }

    func applySnapshot() {
        print("appleSnapshot")
        let sections = Section.allCases
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(sections)
        print(sections)
        print(snapshot)

        let consultItems = self.consultList.map { Item(consult: $0) }
        var consultSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        consultSnapshot.append(consultItems)

        let companyItems = self.companyList.map { Item(company: $0) }
        companySnapshot.append(companyItems)

        let expertItems = self.expertList.map { Item(expert: $0) }
        var expertSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        expertSnapshot.append(expertItems)

        dataSource.apply(consultSnapshot, to: .consult, animatingDifferences: false)
        dataSource.apply(companySnapshot, to: .company, animatingDifferences: false)
        dataSource.apply(expertSnapshot, to: .expert, animatingDifferences: false)
    }
}


추가적으로 알아본 것

  1. 셀이 무한 스크롤 되는 경우 dataSource와 layout이 어떻게 호출되는 지 확인
  2. 섹션 사이에 다른 섹션이 들어갈 수 있는지 확인


1. 셀이 무한 스크롤 되는 경우 dataSource와 layout이 어떻게 호출되는 지 확인

결과적으로 해당 레이아웃과 데이터소스를 불러오는 메소드가 계속해서 호출되는 것을 볼 수 있었습니다.

하단에 아래와 같은 코드를 추가해보았습니다.

func appendCompany() {
    print("appendCompany")
    let companyItems = self.companyList.map { Item(company: $0) }
    companySnapshot.append(companyItems)
    dataSource.apply(companySnapshot, to: .company, animatingDifferences: false)
}

저는 Company Section에서 데이터 뒤에 계속해서 무한스크롤이 되게 만들어보았습니다.

그리고 확인해보니 해당 섹션의 셀이 무한 스크롤 될때마다 전체레이아웃은 계속 불려오고 해당 섹션의 Company DataSource가 계속해서 호출되는것을 발견할 수 있었습니다. 상당히 비효율적임을 볼 수 있겠죠.


2. 섹션 사이에 다른 섹션이 들어갈 수 있는지 확인

결과적으로는 불가능하다 입니다.

이를 정 가능하게 하고싶다면, 임의로 제가 데이터를 잘라서 섹션을 구분해 넣어 만드는 방식입니다.
아래와 같은 방식이라면 가능합니다.

import UIKit
import SwiftyJSON

class ViewController: UIViewController {

    enum Section: Int, Hashable, CaseIterable, CustomStringConvertible {
        case consult1, company, consult2, expert

        var description: String {
            switch self {
            case .consult1: return "Consult"
            case .company: return "Company"
            case .consult2: return "Consult"
            case .expert: return "Expert"
            }
        }
    }

    struct Item: Hashable {
        let expert: Expert?
        let consult: Consult?
        let company: Company?

        init(consult: Consult? = nil, company: Company? = nil, expert: Expert? = nil) {
            self.expert = expert
            self.consult = consult
            self.company = company
        }

        private let identifier = UUID()
    }

    var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    @IBOutlet weak var collectionView: UICollectionView!
    var companyList = Array<Company>()
    var consultList = Array<Consult>()
    var expertList = Array<Expert>()

    var companySnapshot = NSDiffableDataSourceSectionSnapshot<Item>()

    override func viewDidLoad() {
        overrideUserInterfaceStyle = .light
        super.viewDidLoad()

        setData()
        setNavigation()
        createDataSource()
        self.collectionView.collectionViewLayout = self.createLayout()
        createCell()
        applySnapshot()
    }

    func setData() {
        print("setData")
        if let path = Bundle.main.path(forResource: "Home", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { print("path : \(path)")
            do {
                let jsonResult = try JSON(data: data)

                let companyResult = jsonResult["companyInfoList"]
                self.companyList = companyResult.arrayValue.compactMap({Company($0)})

                let consultResult = jsonResult["consultList"]
                self.consultList = consultResult.arrayValue.compactMap({Consult($0)})

                let expertResult = jsonResult["expertList"]
                self.expertList = expertResult.arrayValue.compactMap({Expert($0)})

            }catch {
                print("error : \(error)")
            }
        }
    }

    func createLayout() -> UICollectionViewLayout {
        print("createLayout")
        let sectionProvider = { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }
            let section: NSCollectionLayoutSection
            if sectionKind == .consult1 || sectionKind == .consult2 {
                print("layout consult")
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)

                let groupHeight = NSCollectionLayoutDimension.absolute(200)
                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: groupHeight)
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

                section = NSCollectionLayoutSection(group: group)

            } else if sectionKind == .company {
                print("layout company")
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(350), heightDimension: .absolute(200))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
                section = NSCollectionLayoutSection(group: group)
                section.interGroupSpacing = 10
                section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
            } else if sectionKind == .expert {
                print("layout expert")
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(150), heightDimension: .absolute(150))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
                section = NSCollectionLayoutSection(group: group)
                section.interGroupSpacing = 10
                section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
            }
            else {
                fatalError("Unknown section!")
            }
            return section
        }
        return UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)
    }

    func createCell() {
        print("createCell")
        self.collectionView.register(CompanyCollectionViewCell.self, forCellWithReuseIdentifier: "CompanyCell")
        self.collectionView.register(ConsultCollectionViewCell.self, forCellWithReuseIdentifier: "ConsultCell")
        self.collectionView.register(ExpertCollectionViewCell.self, forCellWithReuseIdentifier: "ExpertCell")
    }

    func createDataSource() {
        print("createDataSource")
            self.dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: self.collectionView) {(collectionView, indexPath, item) -> UICollectionViewCell? in
            guard let section = Section(rawValue: indexPath.section) else { fatalError() }
            switch section {
            case .company:
                print("datasource company")
                if indexPath.row == self.companyList.count - 1 {
                    self.appendCompany()
                }
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CompanyCell", for: indexPath) as? CompanyCollectionViewCell else { preconditionFailure() }
                cell.configure(item.company!)
                return cell
            case .consult1, .consult2:
                print("datasource consult")
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConsultCell", for: indexPath) as? ConsultCollectionViewCell else { preconditionFailure() }
                cell.configure(item.consult!)
                return cell
            case .expert:
                print("datasource expert")
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ExpertCell", for: indexPath) as? ExpertCollectionViewCell else { preconditionFailure() }
                cell.configure(item.expert!)
                return cell
            }
        }
    }

    func setNavigation() {
        navigationItem.title = "홈"
        if #available(iOS 15, *) {
            let appearance = UINavigationBarAppearance()
            appearance.configureWithTransparentBackground()
            appearance.backgroundColor = .systemGreen
            navigationItem.scrollEdgeAppearance = appearance
            navigationItem.standardAppearance = appearance
            navigationItem.compactAppearance = appearance
            navigationController?.setNeedsStatusBarAppearanceUpdate()
        }

    }

    func applySnapshot() {
        print("appleSnapshot")
        let sections = Section.allCases
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(sections)
        print(sections)
        print(snapshot)

        let consultCount = self.consultList.count
        let consultItems1 = self.consultList[0..<consultCount/2].map { Item(consult: $0) }
        var consultSnapshot1 = NSDiffableDataSourceSectionSnapshot<Item>()
        consultSnapshot1.append(consultItems1)

        let companyItems = self.companyList.map { Item(company: $0) }
        companySnapshot.append(companyItems)

        let consultItems2 = self.consultList[consultCount/2..<consultCount].map { Item(consult: $0) }
        var consultSnapshot2 = NSDiffableDataSourceSectionSnapshot<Item>()
        consultSnapshot2.append(consultItems2)

        let expertItems = self.expertList.map { Item(expert: $0) }
        var expertSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        expertSnapshot.append(expertItems)

        dataSource.apply(consultSnapshot1, to: .consult1, animatingDifferences: false)
        dataSource.apply(companySnapshot, to: .company, animatingDifferences: false)
        dataSource.apply(consultSnapshot2, to: .consult2, animatingDifferences: false)
        dataSource.apply(expertSnapshot, to: .expert, animatingDifferences: false)
    }

    func appendCompany() {
        print("appendCompany")
        let companyItems = self.companyList.map { Item(company: $0) }
        companySnapshot.append(companyItems)
        dataSource.apply(companySnapshot, to: .company, animatingDifferences: false)
    }

}

위 코드와 같이 기존의 Consult Section을 consult1, consult2 로 나누어 그 사이에 원하는 섹션 데이터를 넣는 것은 가능합니다. 그러나 이는 근본적으로 섹션안에 섹션을 넣는 방식은 아니죠.

Advances CollectionvView

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


이전 글에서는 CustomCell을 register해서 사용하는 방식을 보여드렸습니다.
사실 커스텀셀을 사용하는 방법은 구글에 검색을 해봐도 잘 나오지 않더군요..

있어도 제 입맛에 맞게 수정하는것에 하나하나 제약사항도 너무 많았구요.

CustomCell 로 보는 Advances CollectionView 포스팅 바로가기

이번에는 애플에서 기본적으로 제공해주는 방식으로 셀을 구성해 데이터를 보여주는 컬렉션뷰를 한번 보여드리도록 하겠습니다.

코드는 애플에서 제공해주는 코드입니다.

Apple Developer


Advances CollectionvView

우선 모델 코드부터 보여드리겠습니다.

  • Emoji 구조체는 Hashable 프로토콜을 채택
  • 각 카테고리에 해당하는 케이스, 리턴값을 정의
import UIKit

struct Emoji: Hashable {

    enum Category: CaseIterable, CustomStringConvertible {
        case recents, smileys, nature, food, activities, travel, objects, symbols
    }

    let text: String
    let title: String
    let category: Category
    private let identifier = UUID()
}

extension Emoji.Category {

    var description: String {
        switch self {
        case .recents: return "Recents"
        case .smileys: return "Smileys"
        case .nature: return "Nature"
        case .food: return "Food"
        case .activities: return "Activities"
        case .travel: return "Travel"
        case .objects: return "Objects"
        case .symbols: return "Symbols"
        }
    }

    var emojis: [Emoji] {
        switch self {
        case .recents:
            return [
                Emoji(text: "🤣", title: "Rolling on the floor laughing", category: self),
                Emoji(text: "🥃", title: "Whiskey", category: self),
                Emoji(text: "😎", title: "Cool", category: self),
                Emoji(text: "🏔", title: "Mountains", category: self),
                Emoji(text: "⛺️", title: "Camping", category: self),
                Emoji(text: "⌚️", title: " Watch", category: self),
                Emoji(text: "💯", title: "Best", category: self),
                Emoji(text: "✅", title: "LGTM", category: self)
            ]

        case .smileys:
            return [
                Emoji(text: "😀", title: "Happy", category: self),
                Emoji(text: "😂", title: "Laughing", category: self),
                Emoji(text: "🤣", title: "Rolling on the floor laughing", category: self)
            ]

        case .nature:
            return [
                Emoji(text: "🦊", title: "Fox", category: self),
                Emoji(text: "🐝", title: "Bee", category: self),
                Emoji(text: "🐢", title: "Turtle", category: self)
            ]

        case .food:
            return [
                Emoji(text: "🥃", title: "Whiskey", category: self),
                Emoji(text: "🍎", title: "Apple", category: self),
                Emoji(text: "🍑", title: "Peach", category: self)
            ]
        case .activities:
            return [
                Emoji(text: "🏈", title: "Football", category: self),
                Emoji(text: "🚴‍♀️", title: "Cycling", category: self),
                Emoji(text: "🎤", title: "Singing", category: self)
            ]

        case .travel:
            return [
                Emoji(text: "🏔", title: "Mountains", category: self),
                Emoji(text: "⛺️", title: "Camping", category: self),
                Emoji(text: "🏖", title: "Beach", category: self)
            ]

        case .objects:
            return [
                Emoji(text: "🖥", title: "iMac", category: self),
                Emoji(text: "⌚️", title: " Watch", category: self),
                Emoji(text: "📱", title: "iPhone", category: self)
            ]

        case .symbols:
            return [
                Emoji(text: "❤️", title: "Love", category: self),
                Emoji(text: "☮️", title: "Peace", category: self),
                Emoji(text: "💯", title: "Best", category: self)
            ]

        }
    }
}


모델은 단순합니다.

위에서 정리했던 것처럼 Emoji 구조체는 Hashable 프로토콜을 채택하고 있습니다.
그 이유는 전에도 말씀드렸던 것처럼 Diffable DataSource는 넘어오는 데이터의 고유함을 반드시 지켜주어야하기 때문입니다.

그리고 아래는 Emoji.Category에 들어가는 각 케이스별 리턴값들을 정의해주고 있습니다.


Section과 Item

실제 우리가 보여줄 Section과 Item을 정의해봅니다.

import UIKit

class ViewController: UIViewController {
    enum Section: Int, Hashable, CaseIterable, CustomStringConvertible {  
        // 섹션에 들어갈 데이터 정의
        case recents, outline, list, custom

        var description: String {
            switch self {
            case .recents: return "Recents"
            case .outline: return "Outline"
            case .list: return "List"
            case .custom: return "Custom"
            }
        }
    }

    struct Item: Hashable {  
        // 섹션 속 아이템에 들어갈 데이터 정의
        let title: String?
        let emoji: Emoji?
        let hasChild: Bool

        init(emoji: Emoji? = nil, title: String? = nil, hasChild: Bool = false) {
            self.emoji = emoji
            self.title = title
            self.hasChild = hasChild
        }

        private let identifier = UUID()
    }
}
  • caseIterable > 배열과 같이 순회 가능
  • customStringConveertible > 사용자 정의에 따른 텍스트 출력 가능


UICollectionView 정의

var collectionView: UICollectionView!

func configureHierarchy() {
    collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
    collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    collectionView.backgroundColor = .systemGroupedBackground
    view.addSubview(collectionView)
}

collectionView 객체를 만들어주고 실제 화면에 정의해주는 부분입니다.
해당 컬렉션 뷰 위에 이제 우리가 보여주고싶은 데이터를 뿌려주게 됩니다.


Create Section Layout

이제 각 섹션들의 레이아웃을 정의해줍니다.

func createLayout() -> UICollectionViewLayout {
    let sectionProvider = { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
        guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }
        let section: NSCollectionLayoutSection

        if sectionKind == .recents {
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.28), heightDimension: .fractionalHeight(0.1))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

            section = NSCollectionLayoutSection(group: group)
            section.interGroupSpacing = 0
            section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
            section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)

        } else if sectionKind == .outline {
            var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
            configuration.leadingSwipeActionsConfigurationProvider = { [weak self] (indexPath) in
                guard let self = self else { return nil }
                guard let item = self.dataSource.itemIdentifier(for: indexPath) else { return nil }
                return self.leadingSwipeActionConfigurationForListCellItem(item)
            }
            section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)

        } else if sectionKind == .list {
            var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
            configuration.leadingSwipeActionsConfigurationProvider = { [weak self] (indexPath) in
                guard let self = self else { return nil }
                guard let item = self.dataSource.itemIdentifier(for: indexPath) else { return nil }
                return self.leadingSwipeActionConfigurationForListCellItem(item)
            }
            section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)

        } else if sectionKind == .custom {
            section = NSCollectionLayoutSection.list(using: .init(appearance: .plain), layoutEnvironment: layoutEnvironment)
            section.contentInsets = NSDirectionalEdgeInsets(top: 100, leading: 10, bottom: 0, trailing: 10)

        } else {
            fatalError("Unknown section!")
        }
        return section
    }
    return UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)
}

각 레이아웃의 환경은 애플에서 제공해주는 요소들로 정의되어져 있습니다.


여기서 중요하게 보아야 할 개념은 sectionKind가 outline, list일때 입니다.

var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)

이때 등장하는 UICollectionLayoutListConfiguration 이건 무엇일까요?

iOS14에서는 UICollectionLayoutListConfiguration 라는 새로운 유형을 제공합니다.
list configuration은 테이블뷰 스타일(.plain, .grouped, insetGrouped)과 같은 모양을 제공합니다.

또한 콜렉션 뷰 List 전용 .sideBar, .sideBarPlain 이라는 새로운 스타일 또한 제공함으로써 다중 열 앱을 구축할 수 있게 되었습니다.

따라서 위와 같은 configuration들을 지정해줌으로써 컬렉션뷰에서도 다양한 리스트 형태의 레이아웃을 만들어 낼 수 있게 된것입니다.

그게 바로 지금 저희가 만들 앱 화면의 모습이기도 합니다.


Create Cell and Resister

이제 컬렉션 뷰 내 보여질 섹션과 아이템들의 레이아웃 구성을 하였으니 본격적으로 셀을 만들어주고 지정해보도록 합니다.

// recent > grid cell registration
func createGridCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewCell, Emoji> {
    // 셀 등록 > iOS14부터는 cell registration을 통해 새롭게 cell을 구성할 수 있음
    return UICollectionView.CellRegistration<UICollectionViewCell, Emoji> { (cell, indexPath, emoji) in
        // 테이블 뷰와 같이 셀에 대한 표준화된 레이아웃을 제공
        var content = UIListContentConfiguration.cell()
        content.text = emoji.text
        content.textProperties.font = .boldSystemFont(ofSize: 38)
        content.textProperties.alignment = .center
        content.directionalLayoutMargins = .zero
        cell.contentConfiguration = content
        var background = UIBackgroundConfiguration.listPlainCell()
        background.cornerRadius = 8
        background.strokeColor = .systemGray3
        background.strokeWidth = 1.0 / cell.traitCollection.displayScale
        cell.backgroundConfiguration = background
    }
}

// outline header cell registration
func createOutlineHeaderCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, String> {
    return UICollectionView.CellRegistration<UICollectionViewListCell, String> { (cell, indexPath, title) in
        var content = cell.defaultContentConfiguration()
        content.text = title
        cell.contentConfiguration = content
    }
}

// outline cell registration
func createOutlineCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, Emoji> {
    return UICollectionView.CellRegistration<UICollectionViewListCell, Emoji> { (cell, indexPath, emoji) in
        var content = cell.defaultContentConfiguration()
        content.text = emoji.text
        content.secondaryText = emoji.title
        cell.contentConfiguration = content
    }
}

// list > list cell registration
func createListCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
    return UICollectionView.CellRegistration<UICollectionViewListCell, Item> { [weak self] (cell, indexPath, item) in
        guard let self = self, let emoji = item.emoji else { return }
        var content = UIListContentConfiguration.valueCell()
        content.text = emoji.text
        content.secondaryText = String(describing: emoji.category)
        cell.contentConfiguration = content
    }
}

이때 또 새로운 개념이 보이죠?

  1. UIListContentConfiguration
  2. defaultContentConfiguration

UIListContentConfiguration은 list based content view에 대한 content configuration을 의미합니다.

defaultContentConfiguration은 우리가 기본적으로 테이블뷰 혹은 컬렉션뷰를 사용할때 각 셀안에 이미 존재하는 textLabel, imageView등을 사용하였습니다. 근데 이제 이러한 접근이 iOS14에서부터는 deprecated 되어 접근이 불가능해졌습니다.

그래서 이때 셀에 각 데이터에 접근하기 위해 defaultContentConfiguration에 접근해야합니다.

그리고 이제 셀을 등록하기 위해 UICollectionView.CellRegistration를 통해 등록하는 것을 볼 수 있습니다.


Diffable DataSource

이제 이렇게 만들어 놓은 셀에 대한 데이터 작업을 해봅니다.

var dataSource: UICollectionViewDiffableDataSource<Section, Item>!


func configureDataSource() {
    // create registrations up front, then choose the appropriate one to use in the cell provider
    let gridCellRegistration = createGridCellRegistration()
    let listCellRegistration = createListCellRegistration()
    let outlineHeaderCellRegistration = createOutlineHeaderCellRegistration()
    let outlineCellRegistration = createOutlineCellRegistration()
    let createPlaceRegistration = createPlainCellRegistration()

    // data source
    dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
        (collectionView, indexPath, item) -> UICollectionViewCell? in
        guard let section = Section(rawValue: indexPath.section) else { fatalError("Unknown section") }
        switch section {
        // recent
        case .recents:
            return collectionView.dequeueConfiguredReusableCell(using: gridCellRegistration, for: indexPath, item: item.emoji)
        // 맨 아래 list
        case .list:
            return collectionView.dequeueConfiguredReusableCell(using: listCellRegistration, for: indexPath, item: item)
        // outline > header, cell
        case .outline:
            if item.hasChild {
                return collectionView.dequeueConfiguredReusableCell(using: outlineHeaderCellRegistration, for: indexPath, item: item.title!)
            } else {
                return collectionView.dequeueConfiguredReusableCell(using: outlineCellRegistration, for: indexPath, item: item.emoji)
            }

        case .custom:
            return collectionView.dequeueConfiguredReusableCell(using: createPlaceRegistration, for: indexPath, item: item)

        }
    }
}


Snapshot

만들어놓은 데이터소스에 스냅샷을 적용하는 코드입니다.

// 스냅샷 적용
    // NSDiffableDataSourceSnapshot > 데이터 접근, 특정 인덱스에 데이터 삽입 및 삭제 가능 > apply 통해 변경사항 적용
    func applyInitialSnapshots() {
        let sections = Section.allCases
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(sections)
        dataSource.apply(snapshot, animatingDifferences: false)

        // recents
        let recentItems = Emoji.Category.recents.emojis.map { Item(emoji: $0) }
        var recentsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        recentsSnapshot.append(recentItems)

        // list + outlines
        var allSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        var outlineSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()

        for category in Emoji.Category.allCases where category != .recents {
            // append to the "all items" snapshot
            let allSnapshotItems = category.emojis.map { Item(emoji: $0) }
            allSnapshot.append(allSnapshotItems)

            // setup our parent/child relations
            let rootItem = Item(title: String(describing: category), hasChild: true)
            outlineSnapshot.append([rootItem])
            let outlineItems = category.emojis.map { Item(emoji: $0) }
            outlineSnapshot.append(outlineItems, to: rootItem)
        }

        let customItems = Emoji.Category.recents.emojis.map { Item(emoji: $0) }
        var cusomSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        cusomSnapshot.append(customItems)

        dataSource.apply(recentsSnapshot, to: .recents, animatingDifferences: false)
        dataSource.apply(allSnapshot, to: .list, animatingDifferences: false)
        dataSource.apply(outlineSnapshot, to: .outline, animatingDifferences: false)
        dataSource.apply(cusomSnapshot, to: .list, animatingDifferences: false)
    }
}