UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)

版本记录

版本号 时间
V1.0 2019.11.05 星期二

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)

开始

首先看下主要内容

主要内容:在本教程中,您将学习如何使用iOS 13的新的声明性UICollectionViewCompositionalLayout API构建美观,现代的UICollectionView布局。

下面看一下写作环境

Swift 5, iOS 13, Xcode 11

iOS PhotosApp Store应用程序具有一些复杂的布局,这些布局是UICollectionViewFlowLayout所无法提供的,具有多个滚动部分和可变大小的平铺布局。 您将学习如何在自己的照片浏览应用程序中创建这些功能!

注意:本教程中的项目屏幕截图使用iOS 13的新的黑暗模式。 无需执行相同操作即可使用本教程,但如果您不这样做,则您的应用看上去将与屏幕截图有所不同。

在Xcode中打开启动项目。 构建并运行。

您会看到一个功能正常但非常简单的应用程序,它显示相册。 您可以滚动列表以查看相册中的照片,然后点击任意一张照片以更详细地查看图像。

切换到Xcode并快速浏览该项目。打开AppDelegate.swift。当应用启动时,它会将AlbumDetailViewController设置为初始视图控制器。 AlbumDetailViewController的初始化程序将URL指向包含图像的文件夹。

打开AlbumDetailViewController.swiftviewDidLoad()通过调用configureCollectionView()设置基本的UICollectionView来显示PhotoItemCells,后者使用iOS 13的新UICollectionViewDiffableDataSource配置集合视图数据源。

如果您之前没有遇到过UICollectionViewDiffableDataSource,请不要担心!需要注意的重要部分是configureDataSource()调用snapshotForCurrentState(),后者从照片URL列表构建数据源快照。然后,它将快照应用于集合视图数据源。

最后,在文件底部的UICollectionViewDelegate扩展中,您可以看到当用户选择一个项目时,该应用程序导航到PhotoDetailViewController

如果您有兴趣,请打开PhotoDetailViewController.swift。这是一个非常简单的视图控制器类,用于显示图像,没有什么令人兴奋的事情。


Why UICollectionViews? A Brief Recap

请记住,UIKit提供了两个视图类来有效显示大量相似项:UITableViewUICollectionView。 乍一看,它们看起来非常相似-两者都显示一个项目列表,可以选择将其分组。

但是,table view只能显示垂直列表中的项目,但是集合视图具有UICollectionViewLayout,它控制项目在屏幕上的显示方式。

自从iOS 6引入以来,UIKit提供了单个集合视图布局实现UICollectionViewFlowLayout。 更复杂的布局是可能的,但不一定易于制作。

UICollectionViewCompositionalLayout通过提供用于构建复杂布局的简单,灵活,声明性的API来进行更改。


Breaking Down a Layout

在开始构建Photos应用之前,您需要了解一些核心布局概念。

集合视图显示重复的项目。 例如,Contacts应用中的联系人。

许多应用程序将项目分组为Sections,这些部分在逻辑上属于同一项目。 对于Contacts应用程序,不同部分包含姓氏以相同字母开头的所有联系人。

Contacts应用中,情况变得如此复杂。 但是可以创建更复杂的布局。 例如,当在文件夹中查看文件时,WWDC视频应用程序在更宽的显示屏上使用两列布局,而Dropbox应用程序在三列布局上使用三列布局。

iOS 13之前,UIKit附带了一个布局类:UICollectionViewFlowLayout。 任何超出系统内置的布局能力的布局都需要构建自定义布局类,通常需要数百行代码。

现在,Apple通过UICollectionViewCompositionalLayout引入了Groups的概念。 这样可以用最少的代码实现更复杂的布局。

考虑内置的Photos应用中的上述布局。 乍一看,这看起来像一个三列布局。 但是,在仔细检查时,请注意大图像如何从三列布局中获取四张照片的空间。 显然,这里发生了更复杂的事情,而答案是Groups

Groups位于ItemsSections之间,并允许您在Sections中应用不同的布局。


Building Your First Layout

好的,理论就足够了。 是时候建立布局了!

打开AlbumDetailViewController.swift并导航到generateLayout()。 此方法负责生成UICollectionViewCompositionalLayout的实例,您的集合视图用于布置其项目。

这是入门项目中的方法:

func generateLayout() -> UICollectionViewLayout {
  //1
  let itemSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalHeight(1.0))
  let fullPhotoItem = NSCollectionLayoutItem(layoutSize: itemSize)
  //2
  let groupSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(2/3))
  let group = NSCollectionLayoutGroup.horizontal(
    layoutSize: groupSize, 
    subitem: fullPhotoItem, 
    count: 1)
  //3
  let section = NSCollectionLayoutSection(group: group)
  let layout = UICollectionViewCompositionalLayout(section: section)
  return layout
}

这是上面的代码中发生的事情:

  • 1) fullPhotoItem是一个NSCollectionLayoutItem,其宽度和高度的分数为1,这意味着它将填充包含它的组。
  • 2) 接下来,创建一个NSCollectionLayoutGroup,其分数宽度为1,但高度为宽度的2/3(照片的标准长宽比)。 该组包含一个水平item
  • 3) 布局的最后一部分是NSCollectionLayoutSection,在这种情况下,它包含在上一行中创建的单个group

这与您在应用程序中看到的布局相匹配。 每张照片均为全宽度,高度为宽度的2/3。

但是,如果您想要两列布局怎么办? 替换下面的组Group布局代码,更新Group大小的高度和布局组中的项目items数:

let groupSize = NSCollectionLayoutSize(
  widthDimension: .fractionalWidth(1.0),
  heightDimension: .fractionalWidth(1/3))
let group = NSCollectionLayoutGroup.horizontal(
  layoutSize: groupSize, 
  subitem: fullPhotoItem, 
  count: 2
)

现在,您已将组group设置为其原始高度的一半,但使它包含两个项目。 构建并运行项目,并查看布局如何更改。 容易吧?

这看起来已经开始好起来了,但是如果每张照片都有一个可以自己调用的空间会更好吗? UICollectionViewCompositionalLayout支持使用content insets来实现这一点。 创建fullPhotoItem之后,将以下内容添加到generateLayout()中:

fullPhotoItem.contentInsets = NSDirectionalEdgeInsets(
  top: 2, 
  leading: 2, 
  bottom: 2, 
  trailing: 2)

构建并运行您的应用程序。


The Power of Groups

关于现在,您可能正在考虑:Columns and insets都可以做到,但是使用UICollectionViewFlowLayout已经很容易了。 给我展示一些很棒的东西! 不用担心,UICollectionViewCompositionalLayout可以帮到您。

Photos应用程序中相册详细信息视图中最奇妙的部分之一是不同大小的照片的效果。 一旦意识到可以将组嵌套在其他组中,构建此布局非常容易。 这就是您要的效果。

乍一看,这看起来非常复杂,但是您可以将其分解为四个不同的布局,其中两个只是彼此的镜像示例:

  • 1) 全宽度照片。
  • 2) 一张“主”照片,其中包含一对垂直堆叠的较小照片。
  • 3) 一行连续三张较小的照片。
  • 4) 第二种风格的相反。

用以下将在generateLayout()中的let groupSize = ...之前的所有内容替换为:

// We have three row styles
// Style 1: 'Full'
// A full width photo
// Style 2: 'Main with pair'
// A 2/3 width photo with two 1/3 width photos stacked vertically
// Style 3: 'Triplet'
// Three 1/3 width photos stacked horizontally

// First type. Full
let fullPhotoItem = NSCollectionLayoutItem(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(2/3)))

fullPhotoItem.contentInsets = NSDirectionalEdgeInsets(
  top: 2, 
  leading: 2, 
  bottom: 2, 
  trailing: 2)

第一种布局类型很简单:单个图像即屏幕的整个宽度。 通过创建宽度为1.0(全宽)且高度为宽度的2/3的单个项目来实现此目的。

在此下面,添加第二种布局类型:

// Second type: Main with pair
// 3
let mainItem = NSCollectionLayoutItem(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(2/3),
    heightDimension: .fractionalHeight(1.0)))

mainItem.contentInsets = NSDirectionalEdgeInsets(
  top: 2, 
  leading: 2, 
  bottom: 2, 
  trailing: 2)

// 2
let pairItem = NSCollectionLayoutItem(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalHeight(0.5)))

pairItem.contentInsets = NSDirectionalEdgeInsets(
  top: 2, 
  leading: 2, 
  bottom: 2, 
  trailing: 2)

let trailingGroup = NSCollectionLayoutGroup.vertical(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1/3),
    heightDimension: .fractionalHeight(1.0)),
  subitem: pairItem, 
  count: 2)

// 1
let mainWithPairGroup = NSCollectionLayoutGroup.horizontal(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(4/9)),
  subitems: [mainItem, trailingGroup])

第二组由一个mainItem和一对较小的项组成。较小的项目本身是垂直放置的组,包含在由主要项目组成的水平放置的组和包含一对较小项目的组中。

这里的数学有些棘手。您必须记住,每个尺寸都是相对于其父级的。您可能会发现从外层开始并向后进行工作比较容易-代码底部到顶部。

  • 1) 外部组mainWithPairGroup应该为全宽度,因此其分数宽度为1.0。主要项目的高度决定了其高度。这是屏幕宽度的2/3,因此高度需要为第一个布局中全宽照片高度的2/3。如果您还记得高中数学,就会知道2/3中的2/34/9
  • 2) 然后,包含两个垂直堆叠的较小项目的trailing组应为其包含组的宽度和全高的1/3。每个较小的项目应为trailing组的全宽及其高度的一半。
  • 3) 最后,主要项目的宽度应为其包含组的宽度的2/3

接下来,将三元triplet组添加到第二组下面:

// Third type. Triplet
let tripletItem = NSCollectionLayoutItem(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1/3),
    heightDimension: .fractionalHeight(1.0)))

tripletItem.contentInsets = NSDirectionalEdgeInsets(
  top: 2, 
  leading: 2, 
  bottom: 2, 
  trailing: 2)

let tripletGroup = NSCollectionLayoutGroup.horizontal(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(2/9)),
  subitems: [tripletItem, tripletItem, tripletItem])

第三组包含三张水平放置的照片。 这意味着该组应具有完整的分数宽度,但高度应为全尺寸照片的1/32/9。 在组内,每个项目应为全高和宽度的1/3。

现在,添加第四个也是最后一个样式:

// Fourth type. Reversed main with pair
let mainWithPairReversedGroup = NSCollectionLayoutGroup.horizontal(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(4/9)),
  subitems: [trailingGroup, mainItem])

鉴于第四个布局与第二个布局相反,您可以通过更改组中子项目的顺序轻松实现此目的。

要完成新的布局,请使用以下内容替换groupSizegroupsection定义:

let nestedGroup = NSCollectionLayoutGroup.vertical(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(16/9)),
  subitems: [
    fullPhotoItem, 
    mainWithPairGroup, 
    tripletGroup, 
    mainWithPairReversedGroup
  ]
)

let section = NSCollectionLayoutSection(group: nestedGroup)

在这里,您将四种“组”类型添加到垂直堆叠项目的容器Group中。 它们应该占据整个宽度,并且如果您遍历数字,则其高度应等于正常高度的1和7/9或16/9。

很多布局。 但是,当您分解每个部分时,它们都很简单,并且您无需为此布局构建任何复杂的逻辑即可支持多种设备或方向。 继续,构建并运行项目。 旋转一下并尝试一下。 也尝试旋转设备!


Adding Supplementary Items

现代应用程序的一个常见功能是在Item or Section级别的集合视图中向项目添加其他context。 例如,在您的主屏幕上代表一个应用的项目可能在右上角带有一个红色的小徽章,显示未读通知的数量。 或者,Contacts应用程序中的某个部分可能具有标题,指示该部分正在显示哪个字母。

UICollectionViewCompositionalLayout为这些类型的附加项目提供了Supplementary Items API。 现在,您将使用它在照片上添加badge,以表明它正在与云存储系统同步。

AlbumDetailViewController.swift中,导航到configureCollectionView()并在注册照片项单元格后添加以下内容:

collectionView.register(
  SyncingBadgeView.self,
  forSupplementaryViewOfKind: AlbumDetailViewController.syncingBadgeKind,
  withReuseIdentifier: SyncingBadgeView.reuseIdentifier)

这段代码告诉collection view,当请求添加具有相关重用标识符的某种supplementary视图时,它应该使用SyncingBadgeView类。 您以前可能曾经遇到过重用标识符,但是supplementary view kind可能对您来说是新的。 就像重用标识符一样,这只是一个字符串,用作告诉UIKit您要添加哪种视图类型的键。

如果您有兴趣签出同步badge view的实现,请在SharedViews组中打开SyncingBadgeView.swift。 这只是一个显示图像的简单UICollectionReusableView子类。

回到AlbumDetailViewController.swift。 接下来,您需要使用SupplementaryViewProvider配置数据源。 这是一个简单的方法,当传递collection viewkind and index path时,它返回一个可选视图。 在创建数据源之后,将以下内容直接添加到configureDataSource()

dataSource.supplementaryViewProvider = {
  (
  collectionView: UICollectionView,
  kind: String,
  indexPath: IndexPath) 
    -> UICollectionReusableView? in
  // 1
  if let badgeView = collectionView.dequeueReusableSupplementaryView(
    ofKind: kind,
    withReuseIdentifier: SyncingBadgeView.reuseIdentifier,
    for: indexPath) as? SyncingBadgeView {
    // 2
    let hasSyncBadge = indexPath.row % Int.random(in: 1...6) == 0
    badgeView.isHidden = !hasSyncBadge
    return badgeView
  } else {
    fatalError("Cannot create new supplementary")
  }
}

这段代码:

  • 1) 要求collection view出队正确的supplementary view
  • 2) 确定照片是否正在同步(在这种情况下是随机的),如果不想显示,则隐藏徽章。

现在,您需要添加另一部分来显示同步视图。 您需要告诉collection view在哪里显示它!

generateLayout()的顶部,添加以下代码:

// Syncing badge
let syncingBadgeAnchor = NSCollectionLayoutAnchor(
  edges: [.top, .trailing], 
  fractionalOffset: CGPoint(x: -0.3, y: 0.3))
let syncingBadge = NSCollectionLayoutSupplementaryItem(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .absolute(20),
    heightDimension: .absolute(20)),
  elementKind: AlbumDetailViewController.syncingBadgeKind,
  containerAnchor: syncingBadgeAnchor)

该代码将同步视图的布局定义为从顶部和trailing锚定30%,绝对宽度和高度为20点。

最后,您需要将此布局添加到要显示同步标志的任何item。 在我们的情况下,就是代表照片的任何项目。 NSCollectionLayoutItem有一个方便的初始化程序,可以接受一系列supplementary items。 更新fullPhotoItem以利用此初始化程序,如下所示:

let fullPhotoItem = NSCollectionLayoutItem(
  layoutSize: NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(2/3)), 
  supplementaryItems: [syncingBadge])

构建并运行项目。 请记住,supplementary同步项目是随机可见的,因此您可能不会每次都看到它!


Browsing Albums

album详细信息视图开始看起来不错。 当然,大多数人都有一个以上的相册,因此,在下一部分中,您将向该应用添加相册查看视图。

打开AppDelegate.swift并替换获取捆绑包的四行,并使用以下代码设置初始视图控制器:

guard let bundleURL = Bundle.main.url(
  forResource: "PhotoData", 
  withExtension: "bundle") else { 
    return false 
}
let initialViewController = 
  AlbumsViewController(withAlbumsFromDirectory: bundleURL)

这会将AlbumsViewController设置为初始视图控制器。 打开AlbumsViewController.swift。 从结构上讲,该类与您已经修改的AlbumDetailViewController类非常相似。

主要区别是该类显示三个部分,而不只是一个部分。 snapshotForCurrentState()处理用于添加每个部分和相关项目的逻辑。 构建并运行项目。

初始视图控制器显示了六个相册的列表,每个相册代表PhotoData捆绑包中的一个文件夹。 一些专辑会显示在多个部分中。 轻按缩略图即可打开专辑详细信息视图。

按照目前的情况,无法分辨每个部分代表什么。 要解决此问题,您将在每个部分添加header。 在configureCollectionView()中,通过在注册AlbumItemCell之后添加以下代码来注册HeaderView类:

collectionView.register(
  HeaderView.self,
  forSupplementaryViewOfKind: AlbumsViewController.sectionHeaderElementKind,
  withReuseIdentifier: HeaderView.reuseIdentifier)

和以前一样,在补充同步视图中,您需要告诉集合视图的数据源如何生成header。 初始化后添加以下内容到configureDataSource()

dataSource.supplementaryViewProvider = { (
  collectionView: UICollectionView,
  kind: String,
  indexPath: IndexPath) 
    -> UICollectionReusableView? in

  guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
    ofKind: kind,
    withReuseIdentifier: HeaderView.reuseIdentifier,
    for: indexPath) as? HeaderView else { 
      fatalError("Cannot create header view") 
  }

  supplementaryView.label.text = Section.allCases[indexPath.section].rawValue
  return supplementaryView
}

和以前一样,您将supplementary view provider设置为闭包,该闭包在给定collection view, a kind and an index path的情况下返回supplementary view。 在这种情况下,它将返回HeaderView-入门项目中的另一个简单视图。

最后,将generateMyAlbumsLayout()中的section初始化代码替换为:

let headerSize = NSCollectionLayoutSize(
  widthDimension: .fractionalWidth(1.0),
  heightDimension: .estimated(44))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
  layoutSize: headerSize,
  elementKind: AlbumsViewController.sectionHeaderElementKind, 
  alignment: .top)

let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [sectionHeader]

此代码为您的标题设置了全宽度的布局大小,估计为44点高。 您使用估计的高度,因为动态类型设置意味着用户看到的标题文本可能比您预期的大。

接下来,代码定义一个section header,最后将其设置为该节的supplementary item。 构建并运行项目,以查看添加到每个部分顶部的header


More Complex Layouts

让您回想起本教程的开始,您会记得相册浏览视图的每个部分使用不同的布局。 是时候添加了!

在注册AlbumItemCell之后,立即在configureCollectionView()中注册两个新的单元格类开始。

collectionView.register(
  FeaturedAlbumItemCell.self, 
  forCellWithReuseIdentifier: FeaturedAlbumItemCell.reuseIdentifer)
collectionView.register(
  SharedAlbumItemCell.self, 
  forCellWithReuseIdentifier: SharedAlbumItemCell.reuseIdentifer)

用以下代码替换configureDataSource()中数据源尾随闭包的内容:

let sectionType = Section.allCases[indexPath.section]
switch sectionType {
case .featuredAlbums:
  guard let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: FeaturedAlbumItemCell.reuseIdentifer,
    for: indexPath) as? FeaturedAlbumItemCell 
  else { fatalError("Could not create new cell") }

  cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
  cell.title = albumItem.albumTitle
  cell.totalNumberOfImages = albumItem.imageItems.count
  return cell

case .sharedAlbums:
  guard let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: SharedAlbumItemCell.reuseIdentifer,
    for: indexPath) as? SharedAlbumItemCell 
  else { fatalError("Could not create new cell") }

  cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
  cell.title = albumItem.albumTitle
  return cell

case .myAlbums:
  guard let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: AlbumItemCell.reuseIdentifer,
    for: indexPath) as? AlbumItemCell 
  else { fatalError("Could not create new cell") }

  cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
  cell.title = albumItem.albumTitle
  return cell
}

这看起来像很多代码,但实际上,这非常简单。 switch语句独立对待每个节section,在出队和配置它之后返回适当的单元格。

接下来,在generateMyAlbumsLayout()下添加一种方法来为特色专辑生成第一部分布局:

func generateFeaturedAlbumsLayout(isWide: Bool) -> NSCollectionLayoutSection {
  let itemSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(2/3))
  let item = NSCollectionLayoutItem(layoutSize: itemSize)

  // Show one item plus peek on narrow screens,
  // two items plus peek on wider screens
  let groupFractionalWidth = isWide ? 0.475 : 0.95
  let groupFractionalHeight: Float = isWide ? 1/3 : 2/3
  let groupSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(CGFloat(groupFractionalWidth)),                               
    heightDimension: .fractionalWidth(CGFloat(groupFractionalHeight)))
  let group = NSCollectionLayoutGroup.horizontal(
    layoutSize: groupSize, 
    subitem: item, 
    count: 1)
  group.contentInsets = NSDirectionalEdgeInsets(
    top: 5, 
    leading: 5, 
    bottom: 5, 
    trailing: 5)

  let headerSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .estimated(44))
  let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
    layoutSize: headerSize,
    elementKind: AlbumsViewController.sectionHeaderElementKind, 
    alignment: .top)

  let section = NSCollectionLayoutSection(group: group)
  section.boundarySupplementaryItems = [sectionHeader]
  section.orthogonalScrollingBehavior = .groupPaging

  return section
}

该方法的前半部分可能看起来很熟悉。 该代码设置了一个item和一个group,并具有附加功能,可根据传递给方法的isWide参数更改组的宽度。 然后添加一个标题。

该方法最有趣的部分就在最后。 集合视图具有确定其滚动方向的主轴。 在我们的情况下,垂直。 正交轴则成直角-在这种情况下为水平。 您可以通过在正交轴上滚动行为来配置此部分布局,该行为在各组之间进行浏览。 在配置第三部分也是最后一部分之后,将在后面进行详细介绍。

添加以下方法来生成共享相册的布局:

func generateSharedlbumsLayout() -> NSCollectionLayoutSection {
  let itemSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .fractionalWidth(1.0))
  let item = NSCollectionLayoutItem(layoutSize: itemSize)

  let groupSize = NSCollectionLayoutSize(
    widthDimension: .absolute(140),
    heightDimension: .absolute(186))
  let group = NSCollectionLayoutGroup.vertical(
    layoutSize: groupSize, 
    subitem: item, 
    count: 1)
  group.contentInsets = NSDirectionalEdgeInsets(
    top: 5, 
    leading: 5, 
    bottom: 5, 
    trailing: 5)

  let headerSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .estimated(44))
  let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
    layoutSize: headerSize,
    elementKind: AlbumsViewController.sectionHeaderElementKind, 
    alignment: .top)

  let section = NSCollectionLayoutSection(group: group)
  section.boundarySupplementaryItems = [sectionHeader]
  section.orthogonalScrollingBehavior = .groupPaging

  return section
}

共享相册的布局相当简单,展现出与精选相册相同的分页行为,但项目尺寸较小。

最后,替换generateLayout()以调用刚刚添加的两个新方法:

func generateLayout() -> UICollectionViewLayout {
  let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
    layoutEnvironment: NSCollectionLayoutEnvironment) 
      -> NSCollectionLayoutSection? in
    let isWideView = layoutEnvironment.container.effectiveContentSize.width > 500

    let sectionLayoutKind = Section.allCases[sectionIndex]
    switch (sectionLayoutKind) {
    case .featuredAlbums: return self.generateFeaturedAlbumsLayout(
      isWide: isWideView)
    case .sharedAlbums: return self.generateSharedlbumsLayout()
    case .myAlbums: return self.generateMyAlbumsLayout(isWide: isWideView)
    }
  }
  return layout
}

请注意,使用layoutEnvironment变量可根据内容的宽度修改布局。

构建并运行项目。

旋转设备以查看布局如何随着不同的宽度而变化:

还记得添加到精选和共享相册中的页面调度正交滚动行为吗? 尝试从左向右滚动这些部分。

UICollectionViewCompositionalLayout之前,要实现这种功能,需要在主视图中添加另一个collection view,并处理各种复杂的交互逻辑。 现在,只需一行代码!

希望您喜欢iOS 13上提供的新的compositional layout API。您可以在Apple官方文档official Apple documentation以及2019年WWDC的精彩演讲excellent session中更深入地了解它们。

后记

本篇主要讲述了基于UICollectionViewCompositionalLayout API的UICollectionViews布局,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容