WWDC19 - Advances in Collection View Layout
2023. 4. 21. 03:34ㆍIOS/WWDC
반응형
WWDC19 - Advances in Collection View Layout
- 기존
UICollectionViewFlowLayout
의 경우 일반적인 디자인에 매우 유용하며 라인 기반으로 작동을 하였습니다. - 그러나 최근 다양한 앱들은 각 레이아웃 별로 커스텀되어 있으므로 매우 복잡한 형태를 가지고 있습니다.
- 다양한 레이아웃을 가진 앱을 만들기 위해서는 퍼포먼스와 뷰가 어떻게 꾸며져야할지 크기는 어느정도로 해야될지에 대한 여러 문제가 발생합니다.
- 이를 해결하기 위해 콘크리트 레이아웃(고정되어 있는 레이아웃?)을 만들었으며 이것이
Compositional Layout
입니다.
Compositional Layout
- 여러 구성이 가능하며
- 유연하고
- 빠릅니다.
func createLayout() -> UICollectionViewLayout {
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
- 해당 코드는 자연스러운 연결을 보여줍니다. 아이템 -> 그룹 -> 섹션 -> 레이아웃
- 우리가 구성한 레이아웃은 다음과 같은 모습으로 보여질것입니다.
- 가장 큰 레이아웃에 섹션이 있고 섹션 안에 그룹, 그룹안에 아이템들이 들어있는 형태로 보여집니다.
NSCollectionLayoutSize
convenience init(
widthDimension width: NSCollectionLayoutDimension,
heightDimension height: NSCollectionLayoutDimension
)
- 해당 타입은 사이즈를 쉽게 추론할 수 있도록 만들어져있는 타입입니다.
- 여기서 너비와 높이는 단순한
Float
과 같은 값이 아니라 Dimension타입이며- fractionWidth
- fractionHeight
- Absolute
- Estimated
- 4가지로 존재합니다.
fractionWidth fractionHeight
- 특정 상위 유닛의 크기에 따른 비율로 줄 수 있는 타입입니다.
.fractionWidth(0.5)
- 우리는 이렇게 전체 너비의 1/2라는 값을 줄 수 도 있고
.fractionHeight(0.3)
- 전체 높이의 30%라는 값을 줄 수도 있습니다.
absolute
- 가장 단순하고 강력한 형태입니다.
.absolute(200)
- 만약 200이라는 높이 혹은 너비를 꼭 가져야하는 불변한 값이라면
absolute
를 사용하면 됩니다.
estimated
- 이름 그대로 추정치 입니다.
- 해당 크기가 얼마나 커질지는 대략적인 값만을 확인하고 시간이 지남에 따라 렌더링 이후에 크기가 커지거나 작아지는 형태로 사용됩니다.
.estimated(200)
NSCollectionLayoutItem
class NSCollectionLayoutItem {
convenience init(layoutSize: NSCollectionLayoutSize)
var contentInsets: NSDirectionEdgeInsets
}
- 말 그대로 아이템으로 하나의 셀이라고 볼 수 있습니다.
NSCollectionLayoutGroup
- 레이아웃의 기본적인 유닛으로 Horizontal, Vertical, Custom 세가지 형태를 가지고 있습니다.
class NSCollectionLayoutGroup: NSCollectionLayoutItem {
class func horizontal(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self
class func vertical(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self
class func custom(layoutSize: NSCollectionLayoutSize, itemProvider: NSCollectionLayoutGroupCustomItemProvider) -> Self
}
NSCollectionLayoutSection
- CollectionView의 섹션별 레이아웃에 대한 정의이며 DataSource쪽에 직접 매핑됩니다.
class NSCollectionLayoutSection {
convenience init(layoutGroup: NSCollectionLayoutGroup)
var contentInsets: NSDirectionalEdgeInsets
}
UICollectionViewCompositionalLayout
- 컬렉션뷰 레이아웃의 최종 타입이며, iOS와 tvOS에 존재하는 타입입니다.
- macOS의 경우
NSCollectionViewCompositionalLayout
이 존재합니다.
class UICollectionViewCompositionalLayout: UICollectionViewLayout {
init(section: NSCollectionLayoutSection)
init(sectionProvider: @escaping SectionProvider)
}
Basic Demo
- 흥미로운 것, 필요한 것들만 정리했습니다.
TwoColumnViewController
- 위와 같이 하나의 column에 두개의 아이템을 넣을려면 어떻게 해야할까요?
- 단순히 아이템 사이즈를
.fractionWidth(0.5)
로 주는 방법도 있겠지만 WWDC에서는 조금 다른 방법을 소개했습니다.
extension TwoColumnViewController {
/// - Tag: TwoColumn
func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
}
.fractionWidth(1.0)
으로 주었지만 Group을 생성할 때 명시적으로NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
하나의 column 당 2개의 아이템을 가지게끔 group을 선언했습니다.- 이를 통해
itemSize
에 정의된.fractionalWidth
는 새롭게 계산되어서 그룹 내에 두개의 아이템이 보이는 형태를 가지게 됩니다.
DistinctSectionsViewController
- 해당 내용은 섹션마다 서로 다른 레이아웃을 가지는 형태를 보여줍니다.
class DistinctSectionsViewController: UIViewController {
enum SectionLayoutKind: Int, CaseIterable {
case list, grid5, grid3
var columnCount: Int {
switch self {
case .grid3:
return 3
case .grid5:
return 5
case .list:
return 1
}
}
}
}
- 해당 섹션마다 서로 다른 형태의 레이아웃을 가지게끔 타입을 생성하며
extension DistinctSectionsViewController {
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
let columns = sectionLayoutKind.columnCount
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 = columns == 1 ?
NSCollectionLayoutDimension.absolute(44) :
NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
return section
}
return layout
}
}
- 위와 같이 구현이 가능합니다.
- 다만 위와 같이 구현했을 경우 가로모드일 때 조금 더 많은 양의 데이터를 하나의 행에 보여주고 싶을 경우가 있을 수 있고
- 혹은, 더 적은 데이터를 보여주고 싶을 경우도 있을 수 있습니다.
- 그럴 경우는 어떻게 해야할까요??
class AdaptiveSectionsViewController: UIViewController {
enum SectionLayoutKind: Int, CaseIterable {
case list, grid5, grid3
func columnCount(for width: CGFloat) -> Int {
let wideMode = width > 800
switch self {
case .grid3:
return wideMode ? 6 : 3
case .grid5:
return wideMode ? 10 : 5
case .list:
return wideMode ? 2 : 1
}
}
}
}
- 이전과 같은 섹션 타입이지만 하나 다른점이 있다면
width
를 받아 값을 반환해준다는 점입니다. - 그러면 레이아웃을 구성할 떄 너비를 어떻게 알고 전달할 수 있을까요?
extension AdaptiveSectionsViewController {
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let layoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
let columns = layoutKind.columnCount(for: layoutEnvironment.container.effectiveContentSize.width)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)
let groupHeight = layoutKind == .list ?
NSCollectionLayoutDimension.absolute(44) : NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
return section
}
return layout
}
}
let columns = layoutKind.columnCount(for: layoutEnvironment.container.effectiveContentSize.width)
NSCollectionLayoutEnviroment
에는 현재 레이아웃이 작동되어야하는 전체 컨테이너의 크기와 같은 정보가 들어있습니다.- 따라서 해당 정보를 이용하여 현재 환경에 적합한 레이아웃을 구성할 수 있습니다.
NSCollectionLayoutSupplementaryItem
- 지금까지 아이템 그룹 섹션과 같은 컬렉션뷰의 메인 뷰에 대해 이야기했다면 이번은 그 외의 아이템들에 관한 이야기입니다.
- Badges, Headers, Footers 세가지 유형으로 구성되어있으며
NSCollectionLayoutSupplementaryItem
을 통해 어떤 것을 사용할 지 어디에 위치할지를 지정해주면 됩니다.
NSCollectionLayoutAnchor
- 해당 사진처럼 단순히 탑과 바텀에만 추가할 수도 있지만
[.trailing, .top]
과 같이 몇가지의 관계를 섞어서 위치를 조정해줄수도 있습니다.
Header & Footer
- 헤더와 푸터뷰를 만들 때 고려해야 될 점은 컨텐츠를 가리지 않는 것입니다.
- 컨텐츠는 그대로 유지하고 그 외의 부분을 확장하여 뷰를 보이게하는 것이 중요합니다.
- 따라서
Boundary supplementary item
이라는 것이 존재하는데 이것은 경계를 토대로 뷰를 추가할 것입니다.
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: SectionHeadersFootersViewController.sectionHeaderElementKind, alignment: .top)
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: SectionHeadersFootersViewController.sectionFooterElementKind, alignment: .bottom)
section.boundarySupplementaryItems = [sectionHeader, sectionFooter]
- 헤더와 풋터는 다음과 같이 만들 수 있으며 만약 콘텐츠 영역에서 특정 뷰를 고정시키고 싶을 때는
header.pinToVisibleBounds = true
를 통해 고정 시킬수도 있습니다. - 그 외에는 `boundarySupplementaryItems에 추가하면 됩니다.
Estimated Self-Sizing
- 우리는 위에서 구현한 header와 footer가 어느정도의 너비를 가진지는 정확하게 알고 있습니다.
- 그러나, 가끔은 텍스트 크기에따라 혹은 뷰 안에 들어가는 이미지 크기에 따라 약간의 높이가 달라지는 것을 원할 때가 분명히 있을겁니다.
let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: SectionHeadersFootersViewController.sectionHeaderElementKind, alignment: .top)
- 우리는 단순히
headerSize
의 높이를estimated
로 주는것 만으로 해결이 가능합니다. - 콘텐츠가 렌더링 될 경우 새로운 높이를 지정해줄 수 있습니다.
Advance Demo
NestedGroupsViewController
- 만약 위와 같이 만들려면 어떻게 해야할까요?
- 하나의 그룹에 좌측에는 화면을 가득채우는 아이템 하나와 우측에는 두개의 아이템으로 구성되어 있는 이 뷰를 만들려면 다음과 같이 만들 수 있습니다.
let leadingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
heightDimension: .fractionalHeight(1.0)))
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let trailingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3)))
trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let trailingGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),
heightDimension: .fractionalHeight(1.0)),
subitem: trailingItem, count: 2)
let nestedGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.4)),
subitems: [leadingItem, trailingGroup])
- 왼쪽의 아이템은 하나만을 생성하고 우측은 하나의 그룹으로 묶기 위해 우측 아이템을 따로 생성하고 하나의 그룹을 만들어 생성해 줍니다.
- 이후 전체 그룹에
leadingItem
과trailingGroup
을 넣어주는 것으로 위 사진과 같이 구현할 수 있습니다.
OrthogonalScrollingViewController
- 위와 같이 구현하기 위해서 페이징 방법에 대해서 6가지 방법이 구현되어 있습니다.
UICollectionLayoutSectionOrthogonalScrollingBehavior | Apple Developer Documentation
UICollectionLayoutSectionOrthogonalScrollingBehavior
- continuous
- continuousGroupLeadingBoundary
- paging
- groupPaging
- groupPagingCentered
- none
- 위 구현으로 이전 스코롤뷰의 영역에 따라 offset을 다르게 주는 형태로 구현을 해야됐던 방법을 탈피했습니다.
반응형
'IOS > WWDC' 카테고리의 다른 글
WWDC22 - Design protocol interfaces in Swift - 1편 (0) | 2023.07.25 |
---|---|
WWDC21 - Your guide to keyboard layout (0) | 2023.04.29 |
WWDC18 - Testing Tips and Tricks - 3 (0) | 2023.04.18 |
WWDC18 - Testing Tips and Tricks - 2(Working with notifications) (0) | 2023.04.12 |
WWDC 18 - Testing Tips & Tricks - 1 (0) | 2023.03.24 |