扬庆の博客

带标签选择功能的列表视图

字数统计: 840阅读时长: 4 min
2021/09/28 Share

带有标签选择的列表界面

应用场景: 评论界面。

使用UICollectionView 布局一个可复选, 自适应宽度的标签选择控件。

步骤:

  • 选用 UICollectionView AutoLayout布局。
  • 外部主控制器传入数据, 刷新, 并加载初始化状态
  • 主控制器刷新layout

UICollectionViewFlowLayout

布局方式, 根据系统返回的[UICollectionViewLayoutAttributes]去动态的计算排列方式 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import UIKit

class DYOptionTagFlowLayout: UICollectionViewFlowLayout {
// item space 间距从外部传入或者写死. 用来计算是否换行时使用.
private let listItemSpace: CGFloat = 8

override init() {
super.init()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func prepare() {
super.prepare()
}

// MARK: - Define area to show and item count
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

let superElementAttributes = super.layoutAttributesForElements(in: rect)

let array = NSArray(array: superElementAttributes!, copyItems: true)

let attributesToReturn = array as! [UICollectionViewLayoutAttributes]

for attributes in attributesToReturn {
if nil == attributes.representedElementKind {
let indexPath = attributes.indexPath
attributes.frame = self.layoutAttributesForItem(at: indexPath)?.frame ?? .zero
}
}
return attributesToReturn
}

// MARK: - Define cell layout
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {

// System calculated attributes
let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as! UICollectionViewLayoutAttributes

// Set inset
let currentSystemFlowlayout = self.collectionView!.collectionViewLayout as! UICollectionViewFlowLayout
let sectionInset = currentSystemFlowlayout.sectionInset

// first item
if indexPath.item == 0 {
var frame = currentItemAttributes.frame
frame.origin.x = sectionInset.left
currentItemAttributes.frame = frame
return currentItemAttributes
}

// Not first item, then need to get one before attributes' frame
let previousIndexPath = NSIndexPath(item: indexPath.item - 1, section: indexPath.section)

let previousFrame = self.layoutAttributesForItem(at: previousIndexPath as IndexPath)?.frame

// Previous and current item frame adjacent points
let previousFrameRightPoint = previousFrame!.origin.x + previousFrame!.size.width + self.listItemSpace

// Current frame
let currentFrame = currentItemAttributes.frame

let strecthedCurrentFrame = CGRect(x: 0, y: currentFrame.origin.y, width: self.collectionView!.frame.size.width, height: currentFrame.size.height)

// Two frame has same area.
let intersect = previousFrame!.intersects(strecthedCurrentFrame)

if !intersect {
// Same line
var frame = currentItemAttributes.frame
frame.origin.x = sectionInset.left
currentItemAttributes.frame = frame
return currentItemAttributes
}

// Other line first item
var frame = currentItemAttributes.frame
frame.origin.x = previousFrameRightPoint
currentItemAttributes.frame = frame
return currentItemAttributes
}
}

// 以上代码, 粘贴复制即可使用.

注意📢

1
2
let array = NSArray(array: superElementAttributes!, copyItems: true)
let attributesToReturn = array as! [UICollectionViewLayoutAttributes]

上面两行代码一定要按照这个格式写, 不然会提示报错, 导致布局失败.

效果图

标签控件

UICollectionView

下面是对collectionView 做一些处理, 让内部 content 自动撑开, 使用 Autolayout 布局.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
open class DYCollectionView: UICollectionView {

public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.translatesAutoresizingMaskIntoConstraints = false
let minimumHeight = heightAnchor.constraint(greaterThanOrEqualToConstant: 1)
minimumHeight.priority = .required
minimumHeight.isActive = true

setContentHuggingPriority(.required, for: .vertical)

}

public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

public override var intrinsicContentSize: CGSize {
return contentSize
}

public override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
layoutIfNeeded()
}
}
}

// 粘贴复制即可使用

📢注意! 一定要是collectionView设置为内部能自动撑开改变高度, 这样刷新cell的时候就不会有问题 .

外部控制器是一个tableview , 自定义cell如下, 将collectionView 嵌套进去.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none

contentView.addSubview(baseStack) // base stack里面放着collectionView.

NSLayoutConstraint.activate([
baseStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
baseStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
baseStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24),
baseStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didSelectPlaceHolder))
placeholderLabel.addGestureRecognizer(tapGesture)

}

// 外部注册这个cell , 然后设置在cellForItem里面进行赋值, 初始化collectionView的数据即可.

当数据改变, 或者需要动态刷新cell高度的时候, 执行一个回调, 在控制器cellForItem中主线程刷新.

1
2
3
4
5
6
7
8
9
10
  cell.editCallBack = {[weak self] in
guard let self = self else { return }
// 主线程刷新 cell layout
UIView.performWithoutAnimation {
self.tableView.beginUpdates()
self.tableView.endUpdates()
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
}
}

所需要的重要代码都已经贴出来了, 这样就完成了一个tableview 中cell 嵌套collectionView 的代码 .

列表视图

CATALOG
  1. 1. 带有标签选择的列表界面
    1. 1.0.1. 步骤:
  2. 1.1. UICollectionViewFlowLayout
    1. 1.1.1. UICollectionView
    2. 1.1.2. 外部控制器是一个tableview , 自定义cell如下, 将collectionView 嵌套进去.