版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.04.26 星期五 |
前言
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框架(十四) —— 如何创建自己的侧滑式面板导航(二)
开始
首先看下写作环境
Swift 4, iOS 11, Xcode 9
下面我们先看一下要实现的效果:
下面我们在慢一点看一下滚动时候的细节
大家可以看到滚动的时候cell的变化效果和一般的colletionViewCell效果是不一样的。
这就是这篇需要做的事情!
UICollectionView
是在iOS 6中引入的,并通过iOS 10
中的新功能进行了改进,是在iOS应用程序中自定义和动画数据集合表示的第一选择。
与UICollectionView
关联的关键实体是UICollectionViewLayout
。 UICollectionViewLayout
对象负责定义集合视图的所有元素的属性,例如单元格,补充视图和装饰视图。
UIKit
提供了一个名为UICollectionViewFlowLayout
的UICollectionViewLayout
的默认实现。此类允许您使用一些基本自定义设置网格布局。
这个UICollectionViewLayout
教程将教你如何子类化和自定义UICollectionViewLayout
类。它还将向您展示如何向集合视图添加自定义补充视图,弹性,粘性和视差效果。
注意:此
UICollectionViewLayout
教程需要Swift 4.0
的中级知识,UICollectionView
的高级知识,仿射变换以及对UICollectionViewLayout
类中核心布局过程如何工作的清晰理解。
如果您不熟悉这些主题,可以阅读Apple官方文档 Apple official documentation…
打开开始项目,你会看到一些可爱的猫头鹰在标准的UICollectionView
中布局,其中的headers
和footers
如下所示:
该应用程序展示了参加2017年丛林足球杯的猫头鹰队的球员。Section headers
显示了他们在球队中的角色,而footer
显示了他们的集体力量。
让我们仔细看看启动项目:
在JungleCupCollectionViewController.swift
文件中,您将找到符合UICollectionDataSource
协议的UICollectionViewController
子类的实现。它实现了所有必需的方法以及添加补充视图的可选方法。
JungleCupCollectionViewController
也采用了MenuViewDelegate
。这是一个让集合视图切换其数据源的协议。
在Reusable Views
文件夹中,单元格有UICollectionViewCell
的子类, section header and section footer
有UICollectionReusableView
。它们链接到Main.storyboard
文件中设计的各自视图。
除此之外,还有CustomLayout
所需的自定义补充视图。 HeaderView
和MenuView
类都是UICollectionReusableView
的子类。它们都链接到自己的.xib
文件。
MockDataManager.swift
文件包含所有团队的数据结构。为方便起见,Xcode项目嵌入了所有必要的资源。
1. Layout Settings
Custom Layout
文件夹值得特别注意,因为它包含两个重要文件:
CustomLayoutSettings.swift
CustomLayoutAttributes.swift
CustomLayoutSettings.swift
实现具有所有布局设置的结构体。 第一组设置处理集合视图的元素大小。 第二组定义布局行为,第三组设置布局间距。
2. Layout Attributes
CustomLayoutAttributes.swift
文件实现名为CustomLayoutAttributes
的UICollectionViewLayoutAttributes
子类。 此类存储集合视图在显示元素之前配置元素所需的所有信息。
它从超类继承了默认属性,如frame,transform,transform3D,alpha
和zIndex
。
它还添加了一些新的自定义属性:
var parallax: CGAffineTransform = .identity
var initialOrigin: CGPoint = .zero
var headerOverlayAlpha = CGFloat(0)
parallax,initialOrigin
和headerOverlayAlpha
是您稍后将在弹性和粘性效果的实现中使用的自定义属性。
注意:布局属性对象可能会被集合视图复制。 因此,在子类化
UICollectionViewLayoutAttributes
时,必须通过实现将自定义属性复制到新实例的适当方法来符合NSCopying
。如果实现自定义布局属性,则还必须覆盖继承的
isEqual
方法以比较属性的值。 从iOS 7
开始,如果这些属性未更改,则集合视图不应用布局属性。
目前,集合视图无法显示所有团队。 目前,老虎队,鹦鹉队和长颈鹿队的支持者不得不等待。
别担心。 他们很快就会回来! CustomLayout
将解决问题。
The Role of UICollectionViewLayout
UICollectionViewLayout
对象的主要目标是提供有关UICollectionView
中每个元素的位置和可视状态的信息。 请记住,UICollectionViewLayout
对象不负责创建单元格或补充视图。 它的工作是为他们提供正确的属性(attributes)
。
创建自定义UICollectionViewLayout
分为三个步骤:
- 1) 对抽象类
UICollectionViewLayout
进行子类化,并声明执行布局计算所需的所有属性。 - 2) 执行所有必需的计算,为每个集合视图的元素提供正确的属性。 这部分将是最复杂的,因为您将从头开始实现
CollectionViewLayout
核心流程。 - 3) 使集合视图采用新的
CustomLayout
类。
Step 1: Subclassing the UICollectionViewLayout Class
在Custom Layout
组中,您可以找到名为CustomLayout.swift
的Swift文件,该文件包含CustomLayout
类存根。 在这个类中,您将实现UICollectionViewLayout
子类和所有Core Layout
进程。
首先,声明CustomLayout
需要计算属性的所有属性。
import UIKit
final class CustomLayout: UICollectionViewLayout {
// 1
enum Element: String {
case header
case menu
case sectionHeader
case sectionFooter
case cell
var id: String {
return self.rawValue
}
var kind: String {
return "Kind\(self.rawValue.capitalized)"
}
}
// 2
override public class var layoutAttributesClass: AnyClass {
return CustomLayoutAttributes.self
}
// 3
override public var collectionViewContentSize: CGSize {
return CGSize(width: collectionViewWidth, height: contentHeight)
}
// 4
var settings = CustomLayoutSettings()
private var oldBounds = CGRect.zero
private var contentHeight = CGFloat()
private var cache = [Element: [IndexPath: CustomLayoutAttributes]]()
private var visibleLayoutAttributes = [CustomLayoutAttributes]()
private var zIndex = 0
// 5
private var collectionViewHeight: CGFloat {
return collectionView!.frame.height
}
private var collectionViewWidth: CGFloat {
return collectionView!.frame.width
}
private var cellHeight: CGFloat {
guard let itemSize = settings.itemSize else {
return collectionViewHeight
}
return itemSize.height
}
private var cellWidth: CGFloat {
guard let itemSize = settings.itemSize else {
return collectionViewWidth
}
return itemSize.width
}
private var headerSize: CGSize {
guard let headerSize = settings.headerSize else {
return .zero
}
return headerSize
}
private var menuSize: CGSize {
guard let menuSize = settings.menuSize else {
return .zero
}
return menuSize
}
private var sectionsHeaderSize: CGSize {
guard let sectionsHeaderSize = settings.sectionsHeaderSize else {
return .zero
}
return sectionsHeaderSize
}
private var sectionsFooterSize: CGSize {
guard let sectionsFooterSize = settings.sectionsFooterSize else {
return .zero
}
return sectionsFooterSize
}
private var contentOffset: CGPoint {
return collectionView!.contentOffset
}
}
这是一个相当大的代码块,但是一旦你将其分解,它就相当简单:
- 1) 枚举是定义
CustomLayout
的所有元素的不错选择。 这可以防止您使用字符串。 还记得黄金法则吗? 没有字符串=没有拼写错误。 - 2)
layoutAttributesClass
计算属性提供了用于属性实例的类。 您必须返回CustomLayoutAttributes
类型的类:在starter
项目中找到的自定义类。 - 3)
UICollectionViewLayout
的子类必须覆盖collectionViewContentSize
计算属性。 - 4)
CustomLayout
需要所有这些属性才能准备属性。 除了settings
之外,它们都是fileprivate
,因为settings
可以由外部对象设置。 - 5)用作语法的计算属性,以避免以后的冗长重复。
现在您已完成声明,您可以专注于核心布局流程实现。
Step 2: Implementing the CollectionViewLayout Core Process
注意:以下代码需要清楚地了解核心布局工作流程
(Core Layout workflow)
。
集合视图直接与CustomLayout
对象一起使用,以管理整个布局过程。例如,集合视图在首次显示或调整大小时会询问布局信息。
在布局过程中,集合视图调用CustomLayout
对象的必需方法。在动画更新等特定情况下可以调用其他可选方法。这些方法可以计算项目的位置,并为集合视图提供所需的信息。
要重写的前两个必需方法是:
prepare()
shouldInvalidateLayout(forBoundsChange:)
prepare()
是您执行确定布局中元素位置所需的任何计算的机会。 shouldInvalidateLayout(forBoundsChange :)
用于定义CustomLayout
对象再次执行核心进程(core process)
的方式和时间。
让我们从实现prepare()
开始。
打开CustomLayout.swift
并将以下扩展名添加到文件末尾:
// MARK: - LAYOUT CORE PROCESS
extension CustomLayout {
override public func prepare() {
// 1
guard let collectionView = collectionView,
cache.isEmpty else {
return
}
// 2
prepareCache()
contentHeight = 0
zIndex = 0
oldBounds = collectionView.bounds
let itemSize = CGSize(width: cellWidth, height: cellHeight)
// 3
let headerAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.header.kind,
with: IndexPath(item: 0, section: 0)
)
prepareElement(size: headerSize, type: .header, attributes: headerAttributes)
// 4
let menuAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.menu.kind,
with: IndexPath(item: 0, section: 0))
prepareElement(size: menuSize, type: .menu, attributes: menuAttributes)
// 5
for section in 0 ..< collectionView.numberOfSections {
let sectionHeaderAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: UICollectionElementKindSectionHeader,
with: IndexPath(item: 0, section: section))
prepareElement(
size: sectionsHeaderSize,
type: .sectionHeader,
attributes: sectionHeaderAttributes)
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let cellIndexPath = IndexPath(item: item, section: section)
let attributes = CustomLayoutAttributes(forCellWith: cellIndexPath)
let lineInterSpace = settings.minimumLineSpacing
attributes.frame = CGRect(
x: 0 + settings.minimumInteritemSpacing,
y: contentHeight + lineInterSpace,
width: itemSize.width,
height: itemSize.height
)
attributes.zIndex = zIndex
contentHeight = attributes.frame.maxY
cache[.cell]?[cellIndexPath] = attributes
zIndex += 1
}
let sectionFooterAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: UICollectionElementKindSectionFooter,
with: IndexPath(item: 1, section: section))
prepareElement(
size: sectionsFooterSize,
type: .sectionFooter,
attributes: sectionFooterAttributes)
}
// 6
updateZIndexes()
}
}
依次对每个注释部分进行说明:
- 1)
Prepare
工作是资源密集型的,可能会影响性能。因此,您将在创建时缓存计算的属性。在执行之前,您必须检查cache
字典是否为空。这对于不弄乱旧的和新的属性attributes
实例至关重要。 - 2) 如果
cache
字典为空,则必须正确初始化它。通过调用prepareCache()
来完成此操作。这将在此解释之后实施。 - 3)
stretchy header
是集合视图的第一个元素。因此,您首先考虑其attributes
。您创建CustomLayoutAttributes
类的实例,然后将其传递给prepareElement(size:type:attributes)
。同样,您稍后将实现此方法。暂时记住每次创建自定义元素时,必须调用此方法才能正确缓存其属性attributes
。 - 4) 粘性菜单是集合视图的第二个元素。您可以像以前一样计算其属性
attributes
。 - 5) 这个循环是核心布局
core layout
过程中最重要的。对于集合视图的每个section
中的每个item
,您:- 为
section's header
创建和准备属性attributes
。 - 创建
items
的属性attributes
。 - 将它们与特定的
indexPath
相关联。 - 计算并设置
item
的frame
和zIndex
。 - 更新
UICollectionView
的contentHeight
。 - 使用
type
(在本例中为单元格)和元素的indexPath
作为键将新创建的属性存储在cache
字典中。 - 最后,您可以为
section's footer
创建和准备属性attributes
。
- 为
- 6) 最后但并非最不重要的是,您调用方法来更新所有
zIndex
值。稍后您将发现有关updateZIndexes()
的详细信息,您将了解为什么这样做非常重要。
接下来,在prepare()
下面添加以下方法:
override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if oldBounds.size != newBounds.size {
cache.removeAll(keepingCapacity: true)
}
return true
}
在shouldInvalidateLayout(forBoundsChange :)
中,您必须定义如何以及何时使prepare()
执行的计算无效。 每次其bounds
属性更改时,集合视图collection view
都会调用此方法。 请注意,每次用户滚动时,集合视图的bounds
属性都会更改。
您始终返回true
,如果bounds size
更改,这意味着集合视图从纵向portrait
模式转换为横向landscape
模式,反之亦然,您也清除缓存字典。
缓存清除是必要的,因为更改设备的方向会触发重新绘制集合视图的frame
。 因此,所有存储的属性都不适合新集合视图的frame
。
接下来,您将实现prepare()
中调用的所有尚未实现方法:
将以下内容添加到扩展程序的底部:
private func prepareCache() {
cache.removeAll(keepingCapacity: true)
cache[.header] = [IndexPath: CustomLayoutAttributes]()
cache[.menu] = [IndexPath: CustomLayoutAttributes]()
cache[.sectionHeader] = [IndexPath: CustomLayoutAttributes]()
cache[.sectionFooter] = [IndexPath: CustomLayoutAttributes]()
cache[.cell] = [IndexPath: CustomLayoutAttributes]()
}
这个方法的第一件事就是清空cache
字典。 接下来,对于每个元素系列,它使用元素类型type
作为主键重置所有嵌套字典。 indexPath
将是用于标识缓存属性(cached attributes)
的辅助(第二)键。
接下来,您将实现prepareElement(size:type:attributes :)
。
将以下定义添加到扩展的末尾:
private func prepareElement(size: CGSize, type: Element, attributes: CustomLayoutAttributes) {
//1
guard size != .zero else {
return
}
//2
attributes.initialOrigin = CGPoint(x:0, y: contentHeight)
attributes.frame = CGRect(origin: attributes.initialOrigin, size: size)
// 3
attributes.zIndex = zIndex
zIndex += 1
// 4
contentHeight = attributes.frame.maxY
// 5
cache[type]?[attributes.indexPath] = attributes
}
以下是对上述情况的逐步说明:
- 1) 检查元素是否具有有效
size
。 如果元素没有size
,则没有理由缓存其属性attributes
- 2) 接下来,将
frame
的origin
值分配给属性的initialOrigin
属性。 为了稍后计算视差和粘性变换,必须备份元素的初始位置。 - 3) 接下来,指定
zIndex
值以防止不同元素之间的重叠。 - 4) 创建并保存所需信息后,更新集合视图的
contentHeight
,因为您已向UICollectionView
添加了新元素。 执行此更新的一种智能方法是将属性的frame maxY
值分配给contentHeight
属性。 - 5) 最后,使用元素
type
和indexPath
作为唯一键将属性attributes
添加到cache
字典中。
最后是时候实现在prepare()
结束时调用的updateZIndexes()
。
将以下内容添加到扩展程序的底部:
private func updateZIndexes(){
guard let sectionHeaders = cache[.sectionHeader] else {
return
}
var sectionHeadersZIndex = zIndex
for (_, attributes) in sectionHeaders {
attributes.zIndex = sectionHeadersZIndex
sectionHeadersZIndex += 1
}
cache[.menu]?.first?.value.zIndex = sectionHeadersZIndex
}
此方法将渐进的zIndex
值分配给section headers
。计数从分配给单元格的最后一个zIndex
开始。最大的zIndex
值分配给菜单的属性attributes
。这种重新分配对于具有一致的粘性行为是必要的。如果未调用此方法,则给定section
的单元格将具有比section headers
更大的zIndex
。这会在滚动时造成难看的重叠效果。
要完成CustomLayout
类并使布局核心进程core process
正常工作,您需要实现一些required
的方法:
layoutAttributesForSupplementaryView(ofKind:at:)
layoutAttributesForItem(at:)
layoutAttributesForElements(in:)
这些方法的目标是在正确的时间为正确的元素提供正确的属性。更具体地,两个第一方法为集合视图提供特定supplementary
视图或特定单元的属性。第三个方法返回给定时刻显示元素的布局属性。
//MARK: - PROVIDING ATTRIBUTES TO THE COLLECTIONVIEW
extension CustomLayout {
//1
public override func layoutAttributesForSupplementaryView(
ofKind elementKind: String,
at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
switch elementKind {
case UICollectionElementKindSectionHeader:
return cache[.sectionHeader]?[indexPath]
case UICollectionElementKindSectionFooter:
return cache[.sectionFooter]?[indexPath]
case Element.header.kind:
return cache[.header]?[indexPath]
default:
return cache[.menu]?[indexPath]
}
}
//2
override public func layoutAttributesForItem(
at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[.cell]?[indexPath]
}
//3
override public func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
visibleLayoutAttributes.removeAll(keepingCapacity: true)
for (_, elementInfos) in cache {
for (_, attributes) in elementInfos where attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
}
下面进行细分:
- 1) 在
layoutAttributesForSupplementaryView(ofKind:at :)
中,您可以打开元素kind
属性并返回与正确kind
和indexPath
匹配的缓存属性attributes
。 - 2) 在
layoutAttributesForItem(at :)
中,您对单元格的属性执行完全相同的操作。 - 3) 在
layoutAttributesForElements(in :)
中,清空visibleLayoutAttributes
数组(您将存储visibile
属性)。 接下来,迭代所有缓存的属性,并仅向数组添加可见元素。 要确定元素是否可见,请测试其frame
是否与集合视图的frame
相交。 最后返回visibleAttributes
数组。
后记
本篇主要讲述了基于自定义UICollectionViewLayout布局的简单示例,感兴趣的给个赞或者关注~~~