์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
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 |
- Xcode
- designpattern
- UIKit
- SnapKit
- ํ๋ก๊ทธ๋๋จธ์ค
- tableView
- visionOS
- swift
- Swfit
- rxcocoa
- BFS
- SwiftUI
- ios
- TCA
- CollectionView
- MVVM
- RxSwift
- ํจ์คํธ์บ ํผ์ค
- arkit
- XCTest
- node.js
- combine
- Flutter
- Kuring
- realm
- Lv2
- ๋ฐฑ์ค
- reactorkit
- BOJ
- raywenderlich
- Today
- Total
lgvv98
[iOS] infinite carousel DiffableDataSource + CompositionalLayout ๋ณธ๋ฌธ
[iOS] infinite carousel DiffableDataSource + CompositionalLayout
๐ฅ ์บ๋ฟ๋งจ 2024. 4. 16. 01:35[iOS] infinite carousel DiffableDataSource + CompositionalLayout
UIKit์์ DiffableDataSource๊ณผ compositonalLayout์ ์ฌ์ฉํ์ฌ ๊ตฌํํด๋ณด์.
์ ์ฒด ์์ค์ฝ๋์ ํด๋น ์ฝ๋์ ๋ํ ์ฃผ์์ ํฌ์คํ ์ ์ผ ํ๋จ์ ์ ๋ถ ๋ฃ์ด๋์์.
์์ ์์
๋ฌดํํ ํ์ ํ๋ ์คํฌ๋กค ๋ทฐ์ ๋ํ ๊ตฌํ ์์ด๋์ด๋ ์ด๋ฏธ ๋ง์ด ์กด์ฌํ๋, ํด๋น ํฌ์คํ
์์๋ diffableDataSource์ ํน์ฑ์ ๋ง๊ฒ๋ ์ ์ฉ
(๊ตฌํ ๋ฐฉ์)
[์์ ๋ถ๋ id๋ง ๋ค๋ฅธ ์๋ณธ] + [์๋ณธ] + [๋ค์ ๋ถ๋ id๋ง ๋ค๋ฅธ ์๋ณธ]์ ๋จผ์ ๊ตฌ์ฑํ ํ ํ๋ฒ์ apply
diffable์ ๊ฒฝ์ฐ์๋ ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋์์ ๋, snapshot์ ์์ ํ๋๊ฒ ์๋ ๋ค์ ์ฐ์.
ํด๋น ๋ถ๋ถ์ iOS 13, 14, 15์ ๋ฐ๋ผ์ ์ฌ์ฉํ ์ ์๋ ๋ฉ์๋ ๋ฑ์ ์ฐจ์ด๊ฐ ์์ผ๋ฏ๋ก ์ ํ ๋คํ๋จผํธ ์ฐธ๊ณ .
์ ์ฒด ์์ ์ฝ๋
//
// InfinityCarouselCollectionView.swift
// DelegatePatternSwiftUI
//
// Created by Geon Woo lee on 4/16/24.
//
import UIKit
import SwiftUI
import SnapKit
#Preview {
TestViewController()
}
class TestViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var originalItems: [MockModel] = [
.init(num: 1, color: .red),
.init(num: 2, color: .orange),
.init(num: 3, color: .yellow)
]
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InfinityCarouselContainerCell.id, for: indexPath) as! InfinityCarouselContainerCell
cell.configure(originalItems)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
.init(width: UIScreen.main.bounds.width, height: 124)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.register(InfinityCarouselContainerCell.self, forCellWithReuseIdentifier: InfinityCarouselContainerCell.id)
cv.dataSource = self
cv.delegate = self
return cv
}()
}
/// InfinityCarouselCell ์
final class InfinityCarouselContainerCell: UICollectionViewCell {
static let id: String = String(describing: InfinityCarouselContainerCell.self)
func configure(_ originalItems: [MockModel]) {
collectionView.configure(originalItems)
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(collectionView)
collectionView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private let collectionView = InfinityCarouselCollectionView()
}
struct MockModel: Hashable {
var id = UUID()
var num: Int
var color: UIColor
}
final class InfinityCarouselCollectionView: UICollectionView {
typealias DiffableDataSource = UICollectionViewDiffableDataSource<Section, MockModel>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, MockModel>
/// ์น์
enum Section {
case main
}
/// ๋ํผ๋ธ ๋ฐ์ดํฐ ์์ค
private var diffableDataSource: DiffableDataSource?
/// ์ค๋ฆฌ์ง๋ ์์ดํ
private var originalItems: [MockModel] = []
func configure(_ originalItems: [MockModel]) {
self.originalItems = originalItems
// โ
์๋ ๋ ํจ์ ํธ์ถ ์์์ ์ฃผ์
configureDiffableDataSource()
updateSnapshot()
}
/// ์ปฌ๋ ์
๋ทฐ์ ๋ค์ด๊ฐ๋ ๋ฐ์ดํฐ์์ค๋ฅผ ๊ตฌ์ฑ
private func configureDiffableDataSource() {
diffableDataSource = .init(collectionView: self) { collectionView, indexPath, itemIdentifier -> UICollectionViewCell? in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: BannerCell.id,
for: indexPath
) as? BannerCell else { return UICollectionViewCell() }
// โ
์๋ค๋ก 3๋ฐฐ ํ์ผ๋ฏ๋ก ์ด๋ ๊ฒ ์ ์ฉ.
let item = self.originalItems[indexPath.row % 3]
cell.configure(item, indexPath)
return cell
}
}
/// ์ค๋
์ท ์
๋ฐ์ดํธ
private func updateSnapshot() {
var snapshot = Snapshot()
snapshot.appendSections([.main])
// โ
ํ์ฅ๋ ์์ดํ
์ ๋ฐฐ์ด์ ๋ฃ๊ณ
var extendedItems = originalItems
for i in 0..<(originalItems.count * 2) {
let item = originalItems[i % 3]
// โ
diffable์ ํน์ฑ์ id๊ฐ ๊ฒน์น๋ฉด ์๋๋ฏ๋ก ์๋ก์ด ๊ฐ์ฒด๋ฅผ ์์ฑํด์ ๋ฃ์ด์ค.
// โ
๋ชจ๋ธ์ด ํฐ ๊ฒฝ์ฐ์๋ id๊ฐ๋ง ๋ฐ๊พธ๋ ์ ๋ต ์ด์ฉ.
extendedItems.append(MockModel(num: item.num, color: item.color))
}
snapshot.appendItems(extendedItems)
self.diffableDataSource?.apply(snapshot, animatingDifferences: true) { [weak self] in
guard let self else { return }
// โ
์ฒ์์ collectionView๊ฐ ๋ํ๋ ๋, ํ ๋ ์ข์ธก์ ์์ดํ
์ด ํ๊ฐ ์ด์์ ์์ด์ผ ํ๋ฏ๋ก ์คํฌ๋กค ์ํ
self.scrollToItem(at: [0, self.originalItems.count],
at: .left,
animated: false)
// ์ปฌ๋ ์
๋ทฐ๊ฐ ๊ทธ๋ ค์ง๊ณ ๋ ํ์ ๋ถ๋ฆผ.
}
}
/// โ
๋ฌดํ ์คํฌ๋กค์ ์ํด visibleItemsInvalidationHandler์์ indexPath๋ฅผ ์กฐ์
private func handleVisibleItemIndexPath(_ indexPath: IndexPath) {
switch indexPath.row {
case self.originalItems.count - 1:
self.scrollToItem(at: [0, self.originalItems.count * 2 - 1], at: .left, animated: false)
case self.originalItems.count * 2 + 1:
self.scrollToItem(at: [0, self.originalItems.count], at: .left, animated: false)
default:
break
}
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: .init())
self.collectionViewLayout = createLayout()
self.register(BannerCell.self, forCellWithReuseIdentifier: BannerCell.id)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// ๋ ์ด์์
private func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { (sectionNumber, env) -> NSCollectionLayoutSection? in
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)))
item.contentInsets = .init(top: 0, leading: 10, bottom: 0, trailing: 10)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.9),
heightDimension: .fractionalHeight(1.0)),
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.contentInsets = .init(top: 12, leading: 0, bottom: 12, trailing: 0)
section.visibleItemsInvalidationHandler = { [weak self] (visibleItems, offset, environment) in
// โ
UIScrollViewDelegate์ didScroll๊ฐ ์๋ ํด๋น ์์ญ์์ ์ฒ๋ฆฌํด์ผ ํจ.
guard let self else { return }
// visibleItems ์ค ๊ฐ์ฅ ๋ง์ง๋ง indexPath๋ฅผ ์ฐพ์์ ๋ฏธ๋ฆฌ ์คํฌ๋กค ํด๋ฌ์ผ ์ ๋๋ฉ์ด์
๋๊ธฐ๋ ๋ฌธ์ ์์.
guard let lastIndexPath = visibleItems.last?.indexPath else { return }
self.handleVisibleItemIndexPath(lastIndexPath)
}
return section
}
}
final class BannerCell: UICollectionViewCell {
static let id: String = String(describing: BannerCell.self)
func configure(_ model: MockModel, _ indexPath: IndexPath) {
contentView.backgroundColor = model.color
contentView.layer.cornerRadius = 16
titleLabel.text = """
์์ดํ
์ ์ซ์ \(model.num)
์ธ๋ฑ์ค ํจ์ค \(indexPath)
"""
}
override init(frame: CGRect) {
super.init(frame: frame)
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var titleLabel: UILabel = {
var lb = UILabel()
lb.textAlignment = .center
lb.numberOfLines = 2
lb.font = .systemFont(ofSize: 22, weight: .medium)
return lb
}()
private func configureUI() {
contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
}
}
'apple > ๐ Apple Docs & iOS & Swift' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Swift] JSON ํํ๋ก ๋ฐ๊พธ๋ ๋ฐฉ๋ฒ (0) | 2024.06.18 |
---|---|
[Xcode 16 Beta] Could not download and install iOS 18.0 Simulator runtime with Xcode 16.0 beta (0) | 2024.06.12 |
[Swift] New access modifier: package (0) | 2024.04.05 |
[Natural Language] ํ ์คํธ ๊ฐ ์ ์ฌ์ ์ฐพ๊ธฐ (0) | 2024.03.27 |
[Natural Language] Overview (0) | 2024.03.27 |