这是一个集合视图布局装饰视图教程在 Swift 中(这是 Swift 3,Xcode 8 种子 6)。
装饰视图不是 UICollectionView 的功能;它们本质上属于 UICollectionViewLayout。没有 UICollectionView 方法(或委托或数据源方法)提及装饰视图。 UICollectionView 对它们一无所知;它只是按照它的指示去做。
要提供任何装饰视图,您将需要一个 UICollectionViewLayout 子类;这个子类可以自由定义自己的属性和委托协议方法来自定义其装饰视图的配置方式,但这完全取决于您。
为了说明这一点,我将对 UICollectionViewFlowLayout 进行子类化,以在集合视图的内容矩形的顶部强加一个标题标签。这可能是对装饰视图的愚蠢使用,但它完美地说明了基本原理。为了简单起见,我将首先对整个事情进行硬编码,使客户端无法自定义该视图的任何方面。
在布局子类中实现装饰视图有四个步骤:
定义 UICollectionReusableView 子类。
使用布局注册 UICollectionReusableView 子类 (not集合视图),通过调用register(_:forDecorationViewOfKind:)
。布局的初始值设定项是执行此操作的好地方。
实施layoutAttributesForDecorationView(ofKind:at:)
返回定位 UICollectionReusableView 的布局属性。要构造布局属性,请调用init(forDecorationViewOfKind:with:)
并配置属性。
覆盖layoutAttributesForElements(in:)
使得结果为layoutAttributesForDecorationView(ofKind:at:)
包含在返回的数组中。
最后一步是导致装饰视图出现在集合视图中的原因。当集合视图调用时layoutAttributesForElements(in:)
,它发现结果数组包含指定类型的装饰视图的布局属性。集合视图对装饰视图一无所知,因此它返回到布局,要求这种装饰视图的实际实例。您已经注册了这种装饰视图以对应于您的 UICollectionReusableView 子类,因此您的 UICollectionReusableView 子类被实例化并返回该实例,并且集合视图根据布局属性来定位它。
那么让我们按照步骤操作吧。定义 UICollectionReusableView 子类:
class MyTitleView : UICollectionReusableView {
weak var lab : UILabel!
override init(frame: CGRect) {
super.init(frame:frame)
let lab = UILabel(frame:self.bounds)
self.addSubview(lab)
lab.autoresizingMask = [.flexibleWidth, .flexibleHeight]
lab.font = UIFont(name: "GillSans-Bold", size: 40)
lab.text = "Testing"
self.lab = lab
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
现在我们转向 UICollectionViewLayout 子类,我将其称为 MyFlowLayout。我们在布局的初始值设定项中注册 MyTitleView;我还定义了剩余步骤所需的一些私有属性:
private let titleKind = "title"
private let titleHeight : CGFloat = 50
private var titleRect : CGRect {
return CGRect(10,0,200,self.titleHeight)
}
override init() {
super.init()
self.register(MyTitleView.self, forDecorationViewOfKind:self.titleKind)
}
实施layoutAttributesForDecorationView(ofKind:at:)
:
override func layoutAttributesForDecorationView(
ofKind elementKind: String, at indexPath: IndexPath)
-> UICollectionViewLayoutAttributes? {
if elementKind == self.titleKind {
let atts = UICollectionViewLayoutAttributes(
forDecorationViewOfKind:self.titleKind, with:indexPath)
atts.frame = self.titleRect
return atts
}
return nil
}
覆盖layoutAttributesForElements(in:)
;这里的索引路径是任意的(我在前面的代码中忽略了它):
override func layoutAttributesForElements(in rect: CGRect)
-> [UICollectionViewLayoutAttributes]? {
var arr = super.layoutAttributesForElements(in: rect)!
if let decatts = self.layoutAttributesForDecorationView(
ofKind:self.titleKind, at: IndexPath(item: 0, section: 0)) {
if rect.intersects(decatts.frame) {
arr.append(decatts)
}
}
return arr
}
这有效!标题标签“测试”出现在集合视图的顶部。
现在我将展示如何使标签可定制。我们将允许客户端设置一个确定标题的属性,而不是标题“测试”。我将把我的布局子类设为 publictitle
财产:
class MyFlowLayout : UICollectionViewFlowLayout {
var title = ""
// ...
}
任何使用此布局的人都应该设置此属性。例如,假设此集合视图显示美国 50 个州:
func setUpFlowLayout(_ flow:UICollectionViewFlowLayout) {
flow.headerReferenceSize = CGSize(50,50)
flow.sectionInset = UIEdgeInsetsMake(0, 10, 10, 10)
(flow as? MyFlowLayout)?.title = "States" // *
}
现在我们遇到了一个奇怪的难题。我们的布局有一个title
属性,其值需要以某种方式传递给我们的 MyTitleView 实例。但这什么时候可能发生呢?我们不负责实例化 MyTitleView;当集合视图在后台请求实例时,它会自动发生。 MyFlowLayout 实例和 MyTitleView 实例没有相遇的时刻。
解决方案是使用布局属性作为信使。 MyFlowLayout 永远不会满足 MyTitleView,但它确实创建了布局属性对象,该对象被传递到集合视图以配置 MyFlowLayout。所以布局属性对象就像一个信封。通过子类化 UICollectionViewLayoutAttributes,我们可以在该信封中包含我们喜欢的任何信息 - 例如标题:
class MyTitleViewLayoutAttributes : UICollectionViewLayoutAttributes {
var title = ""
}
这是我们的信封!现在我们重写我们的实现layoutAttributesForDecorationView
。当我们实例化布局属性对象时,我们实例化我们的子类并设置它的title
财产:
override func layoutAttributesForDecorationView(
ofKind elementKind: String, at indexPath: IndexPath) ->
UICollectionViewLayoutAttributes? {
if elementKind == self.titleKind {
let atts = MyTitleViewLayoutAttributes( // *
forDecorationViewOfKind:self.titleKind, with:indexPath)
atts.title = self.title // *
atts.frame = self.titleRect
return atts
}
return nil
}
最后,在 MyTitleView 中,我们实现了apply(_:)
方法。当集合视图配置装饰视图时,这将被调用 - 使用布局属性对象作为其参数!所以我们拉出title
并将其用作我们标签的文本:
class MyTitleView : UICollectionReusableView {
weak var lab : UILabel!
// ... the rest as before ...
override func apply(_ atts: UICollectionViewLayoutAttributes) {
if let atts = atts as? MyTitleViewLayoutAttributes {
self.lab.text = atts.title
}
}
}
很容易看出如何扩展该示例以使字体和高度等标签功能可自定义。由于我们是 UICollectionViewFlowLayout 的子类,因此可能还需要进行一些进一步的修改,通过下推其他元素来为装饰视图腾出空间。另外,从技术上讲,我们应该覆盖isEqual(_:)
在 MyTitleView 中区分不同的标题。所有这些都留给读者作为练习。