안녕하세요 Skillist입니다.
이번엔 Today 화면을 구현해볼 차례입니다.
UICollectionViewController와 UICollectionViewCompositionalLayout를 활용하여 section을 구성합니다.
저의 코드를 보면, UICollectionViewController와 UICollectionView를 번갈아가며 사용는 것과 같이,
같은 로직이라고 할지라도, 조금씩 다르게 구현합니다.
이유는, 다들 아시죠?. 다양한 경험과 학습을 위함이죠. 이것도 써보고 저것도 써보고~
그럼 시작할게요.
------------------------------------------------------------------------------------------------------------------
UICollectionViewController의 시작입니다.
12라인 : statusBar에 위치할 view입니다. appStore 스크린샷을 잠깐 볼까요?
statusBar의 text 가독성을 위해, 반투명한 view가 추가된것이 보이죠?
저는 UIView를 추가하고, 스크롤 위치에 따른 로직으로 구현했습니다. 혹시 다른 좋은 방법이 있다면 알려주세요!!
스크롤 위치에 따라, background 색상과, isHidden을 설정했어요.
아주 간단하게, statusBarView를 추가하고 위치를 지정했어요.
statusBarView는 디스플레이 최상단부터 safeArea의 top 사이에 추가했습니다.
------------------------------------------------------------------------------------------------------------------
다음은 collectionView를 볼게요.
23라인 : collectionView와 레이아웃(statusBar View 추가)을 설정하고 더미 데이터를 설정합니다.
21라인 : section타입과, 아이템 array를 가지는 세션 array입니다. 일반 collectionView에서 사용하는 단일 array와는 다르죠. section에 따른 item들을 보여주기 위함입니다. 아래에 설명할 코드를 보면, 이해될거에요.
setupCollectionView에선 이름 그대로 collectionView를 설정합니다.
36라인 : 레이아웃은 UICollectionViewCompositionalLayout로 구성하였는데, 코드가 긴편이니 추후에 보겠습니다.
39라인 : 앞서 작성한 Cell들을 등록했습니다.
43라인 : 앞서 작성한 Header를 등록했습니다.
47라인 : section의 배경뷰를 등록했습니다.
------------------------------------------------------------------------------------------------------------------
66라인 : sectionType으로 분기처리 합니다. section마다 다른 레이아웃을 가지기 때문이죠.
createAccountSection은 다음 section을 구현합니다.
아이템과 그룹, 섹션을 설정했어요. NSCollectionLayoutSection에 대한 설명은 곧 작성하도록 노력해볼게요 ㅠㅜㅜ
------------------------------------------------------------------------------------------------------------------
createLargeItemSection은 다음 section을 구현합니다.
------------------------------------------------------------------------------------------------------------------
createSmallItemSection은 다음 section을 구현합니다.
하나의 section에는 4개의 아이템으로 구성된 1개의 그룹이 들어있어요.
https://developer.apple.com/documentation/uikit/nscollectionlayoutsection
apple 공식 문서 한번 봐보세요.
저는 다음과 같이 section을 구현했어요.
Section에는 1개의 Group이 존재하고, Group 안에는 4개의 Item이 존재하는거에요.
또한, Section에는 Header와 Background가 존재합니다.
118라인 : 아이템을 설정합니다.
122라인 : 그룹을 지정합니다. 사이즈를 설정하고, 그룹에 속할 아이템 개수를 설정하죠.
126라인 : section을 지정합니다. insets도 설정했어요.
130라인 : header를 설정합니다.
133라인 : background 를 설정합니다.
header입니다. 이전에 작성한 header뷰입니다.
------------------------------------------------------------------------------------------------------------------
collectionView에 대한 레이아웃 설정을 완료했습니다. 다음으로 collectionView의 dataSource를 볼게요.
152라인 : section의 숫자를 리턴합니다.
156라인 : section의 아이템 숫자를 리턴합니다.
sectionType에 따라, 적절한 cell을 리턴합니다. 물론 data도 설정하죠.
191라인 : header를 리턴합니다. 헤더는 단 하나의 section에서만 사용하기에, 따로 분기처리는 하지 않았습니다. 추후 필요시 적절히 분기 처리 합니다.
------------------------------------------------------------------------------------------------------------------
더미 데이터 설정은 단순하게, 하드코딩하였습니다. 첫번째 위치로, account section을 추가했고, 다른 section들을 추가했습니다.
이렇게 Today 화면 구성을 마쳤습니다. 어때요??
개발 할땐, 약간 복잡하기도 하고, 코드량도 많고 그랬었는데요,
글로 작성해보니, 수정할부분도 있고, 다음 구현시엔 더 깔끔하게 구현할 수 있을것 같아요!!!!
고생하셨고 다음에 또 봐요.
잘못되거나 부족한 내용 등, 피드백 감사합니다!
Skillist의 AppleAppStore 프로젝트
https://github.com/DeveloperSkillist/AppleAppStoreCloneCode
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 전체 코드 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
import UIKit
class TodayCollectionViewController: UICollectionViewController {
private lazy var statusBarView: UIView = {
var view = UIView()
view.backgroundColor = .clear
view.alpha = 0.9
return view
}()
let todaySmallItemSectionBackground = "TodaySmallItemSectionBackground"
let sectionDefaultMargin: CGFloat = 16
var items: [TodayItem] = []
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
setupLayout()
setData()
}
}
extension TodayCollectionViewController {
private func setupCollectionView() {
collectionView.backgroundColor = .systemBackground
collectionView.collectionViewLayout = layout()
//cell
collectionView.register(TodayAccountCollectionViewCell.self, forCellWithReuseIdentifier: "TodayAccountCollectionViewCell")
collectionView.register(TodayLargeItemCollectionViewCell.self, forCellWithReuseIdentifier: "TodayLargeItemCollectionViewCell")
collectionView.register(TodaySmallItemCollectionViewCell.self, forCellWithReuseIdentifier: "TodaySmallItemCollectionViewCell")
//header
collectionView.register(TodaySmallItemCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "TodaySmallItemCollectionViewCell")
//section background
collectionView.collectionViewLayout.register(TodaySmallItemBackgroundView.self, forDecorationViewOfKind: todaySmallItemSectionBackground)
}
private func setupLayout() {
view.backgroundColor = .black
//status bar view
view.addSubview(statusBarView)
statusBarView.snp.makeConstraints {
$0.top.leading.trailing.equalToSuperview()
$0.bottom.equalTo(view.safeAreaLayoutGuide.snp.top)
}
}
}
extension TodayCollectionViewController {
private func layout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout { [weak self] section, _ -> NSCollectionLayoutSection? in
let itemType = self?.items[section].type
switch itemType {
case .accountProfile:
return self?.createAccountSection()
case .largeItem:
return self?.createLargeItemSection()
case .smallItem:
return self?.createSmallItemSection()
default:
return nil
}
}
}
private func createAccountSection() -> NSCollectionLayoutSection {
//item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
//group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(96))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
//section
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .none
section.contentInsets = .init(top: 0, leading: sectionDefaultMargin, bottom: 0, trailing: sectionDefaultMargin)
return section
}
private func createLargeItemSection() -> NSCollectionLayoutSection {
//item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
//group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1.4))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
//section
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .none
section.contentInsets = .init(top: sectionDefaultMargin, leading: sectionDefaultMargin, bottom: sectionDefaultMargin, trailing: sectionDefaultMargin)
return section
}
private func createSmallItemSection() -> NSCollectionLayoutSection {
//item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
//group
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(collectionView.frame.size.width - 32), heightDimension: .estimated(310))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 4)
//section
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .none
section.contentInsets = .init(top: sectionDefaultMargin, leading: sectionDefaultMargin, bottom: sectionDefaultMargin + 30, trailing: sectionDefaultMargin)
let sectionHeader = self.createSamllItemSectionHeader()
section.boundarySupplementaryItems = [sectionHeader]
let sectionBackground = NSCollectionLayoutDecorationItem.background(
elementKind: todaySmallItemSectionBackground)
section.decorationItems = [sectionBackground]
return section
}
private func createSamllItemSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
//section header 사이즈
let layoutSectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(40))
//section header Layout
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
return sectionHeader
}
}
extension TodayCollectionViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items[section].items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let itemType = items[indexPath.section].type
switch itemType {
case .accountProfile:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TodayAccountCollectionViewCell", for: indexPath) as? TodayAccountCollectionViewCell else {
return UICollectionViewCell()
}
return cell
case .largeItem:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TodayLargeItemCollectionViewCell", for: indexPath) as? TodayLargeItemCollectionViewCell else {
return UICollectionViewCell()
}
guard let largeItem = items[indexPath.section].items[indexPath.row] as? TodayLargeItem else {
return UICollectionViewCell()
}
cell.setup(largeItem: largeItem)
return cell
case .smallItem:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TodaySmallItemCollectionViewCell", for: indexPath) as? TodaySmallItemCollectionViewCell else {
return UICollectionViewCell()
}
guard let smallItem = items[indexPath.section].items[indexPath.row] as? TodaySmallItem else {
return UICollectionViewCell()
}
cell.setup(item: smallItem)
return cell
}
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
guard let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "TodaySmallItemCollectionViewCell", for: indexPath) as? TodaySmallItemCollectionHeaderView else {
return UICollectionReusableView()
}
let item = items[indexPath.section]
headerView.setup(mainText: item.mainText, subText: item.subText)
return headerView
}
return UICollectionReusableView()
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let isStatusBarHidden = scrollView.contentOffset.y < 0
if isStatusBarHidden {
statusBarView.backgroundColor = .clear
} else {
statusBarView.backgroundColor = .systemBackground
}
statusBarView.isHidden = isStatusBarHidden
}
}
extension TodayCollectionViewController {
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//TODO: 아이템 터치시 화면 이동
// let item = items[indexPath.section].items[indexPath.row]
// let detailVC = DetailViewController()
// detailVC.modalPresentationStyle = .overFullScreen
// present(detailVC, animated: true, completion: nil)
}
}
extension TodayCollectionViewController {
private func setData() {
items = [
TodayItem(type: .accountProfile, items: ["myAccount"]),
TodayItem(type: .largeItem, items: [
TodayLargeItem(
subText: "함께하는 프로젝트!",
mainText: "Skillist의\n속업오버로드~",
bottomText: "우리 같이 공부해요.",
subTitleColor: .darkGray,
bottomTitlecolor: .darkGray,
imageURL: nil,
image: RandomData.image
)
]),
TodayItem(type: .smallItem, items: [
TodaySmallItem(mainText: "랜덤 skillist", subText: "생각보다 빡세네요.", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image),
TodaySmallItem(mainText: "랜덤 skillist", subText: "코딩량이 많아요", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image),
TodaySmallItem(mainText: "랜덤 skillist", subText: "그래도 재밌어요.", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image),
TodaySmallItem(mainText: "랜덤 skillist", subText: "완전 재밌어요.", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image)
], subText: "Skillist의 앱 목록이에요.", mainText: "대박 대박 앱"),
TodayItem(type: .largeItem, items: [
TodayLargeItem(
subText: "이렇게 하세요.",
mainText: "클론코딩으로 실력을 키우자.",
bottomText: "아주 좋은 방법!",
subTitleColor: .white,
mainTitleColor: .white,
bottomTitlecolor: .white,
imageURL: nil,
image: RandomData.image
)
]),
TodayItem(type: .largeItem, items: [
TodayLargeItem(
subText: "함께하는 프로젝트!",
mainText: "Skillist의\n속업오버로드~",
bottomText: "우리 같이 공부해요.",
subTitleColor: .darkGray,
bottomTitlecolor: .darkGray,
imageURL: nil,
image: RandomData.image
)
]),
TodayItem(type: .smallItem, items: [
TodaySmallItem(mainText: "랜덤 skillist", subText: "생각보다 빡세네요.", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image),
TodaySmallItem(mainText: "랜덤 skillist", subText: "코딩량이 많아요", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image),
TodaySmallItem(mainText: "랜덤 skillist", subText: "그래도 재밌어요.", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image),
TodaySmallItem(mainText: "랜덤 skillist", subText: "완전 재밌어요.", isInAppPurchase: RandomData.boolean, isInstalled: RandomData.boolean, imageURL: nil, image: RandomData.image)
], subText: "Skillist의 앱 목록이에요.", mainText: "대박 대박 앱"),
TodayItem(type: .largeItem, items: [
TodayLargeItem(
subText: "이렇게 하세요.",
mainText: "클론코딩으로 실력을 키우자.",
bottomText: "아주 좋은 방법!",
subTitleColor: .white,
mainTitleColor: .white,
bottomTitlecolor: .white,
imageURL: nil,
image: RandomData.image
)
])
]
}
}
'iOS 개발 > Apple App Store 클론 코딩' 카테고리의 다른 글
AppleAppStore - 7. App 화면 구현 (0) | 2021.12.17 |
---|---|
AppleAppStore - 6. App 화면의 Cell 구현 (0) | 2021.12.17 |
AppleAppStore - 4. Today 화면의 cell 구현 2 (0) | 2021.12.16 |
AppleAppStore - 3. Today 화면의 cell 구현 1 (0) | 2021.12.16 |
AppleAppStore - 2. TabBar 구현 (0) | 2021.12.15 |