哈Ha! 我向您展示了文章“
UICollectionView教程:动态更改演示文稿 ”的翻译。
在本文中,我们将考虑使用各种显示元素的方式,以及它们的重用和动态变化。 在这里,我们将不讨论使用集合和自动布局的基础知识。
结果,我们得到一个例子:
在开发移动应用程序时,通常情况下,表视图不够用,您需要显示更加有趣和独特的元素列表。 此外,更改元素显示方式的功能可以成为应用程序中的“芯片”。
使用UICollectionView和UICollectionViewDelegateFlowLayout协议的各种实现,上述所有功能都很容易实现。
完整的项目代码。首先我们需要实现:
带有UIImageView和UILabel的单元格用于显示水果
我们将使用xib在一个单独的文件中创建单元以供重用。
通过设计,我们看到有2个可能的单元格选项-图像下面和文本右边。


单元格的类型可能完全不同,在这种情况下,您需要创建2个单独的类并使用所需的类。 在我们的例子中,没有这种需要,使用UIStackView的1个单元就足够了。
为单元创建接口的步骤:- 添加UIView
- 在其中添加UIStackView(水平)
- 接下来,将UIImageView和UILabel添加到UIStackView。
- 对于UILabel,为水平和垂直设置“内容压缩抗性优先级”的值= 1000。
- 为UIImageView宽高比添加1:1,并将优先级更改为750。
这对于在水平模式下正确显示是必需的。
接下来,我们编写用于在水平和垂直模式下显示单元格的逻辑。
水平显示的主要标准将是单元格本身的大小。 即 如果有足够的空间-显示水平模式。 如果没有,则垂直。 由于图像应为正方形,因此我们假设宽度为高度的2倍时有足够的空间。
单元格代码: class FruitCollectionViewCell: UICollectionViewCell { static let reuseID = String(describing: FruitCollectionViewCell.self) static let nib = UINib(nibName: String(describing: FruitCollectionViewCell.self), bundle: nil) @IBOutlet private weak var stackView: UIStackView! @IBOutlet private weak var ibImageView: UIImageView! @IBOutlet private weak var ibLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() backgroundColor = .white clipsToBounds = true layer.cornerRadius = 4 ibLabel.font = UIFont.systemFont(ofSize: 18) } override func layoutSubviews() { super.layoutSubviews() updateContentStyle() } func update(title: String, image: UIImage) { ibImageView.image = image ibLabel.text = title } private func updateContentStyle() { let isHorizontalStyle = bounds.width > 2 * bounds.height let oldAxis = stackView.axis let newAxis: NSLayoutConstraint.Axis = isHorizontalStyle ? .horizontal : .vertical guard oldAxis != newAxis else { return } stackView.axis = newAxis stackView.spacing = isHorizontalStyle ? 16 : 4 ibLabel.textAlignment = isHorizontalStyle ? .left : .center let fontTransform: CGAffineTransform = isHorizontalStyle ? .identity : CGAffineTransform(scaleX: 0.8, y: 0.8) UIView.animate(withDuration: 0.3) { self.ibLabel.transform = fontTransform self.layoutIfNeeded() } } }
让我们继续讨论主要部分-控制器以及用于显示和切换单元格类型的逻辑。
对于所有可能的显示状态,创建一个枚举PresentationStyle。
我们还添加了一个按钮,用于在导航栏中切换状态。
class FruitsViewController: UICollectionViewController { private enum PresentationStyle: String, CaseIterable { case table case defaultGrid case customGrid var buttonImage: UIImage { switch self { case .table: return imageLiteral(resourceName: "table") case .defaultGrid: return imageLiteral(resourceName: "default_grid") case .customGrid: return imageLiteral(resourceName: "custom_grid") } } } private var selectedStyle: PresentationStyle = .table { didSet { updatePresentationStyle() } } private var datasource: [Fruit] = FruitsProvider.get() override func viewDidLoad() { super.viewDidLoad() self.collectionView.register(FruitCollectionViewCell.nib, forCellWithReuseIdentifier: FruitCollectionViewCell.reuseID) collectionView.contentInset = .zero updatePresentationStyle() navigationItem.rightBarButtonItem = UIBarButtonItem(image: selectedStyle.buttonImage, style: .plain, target: self, action: #selector(changeContentLayout)) } private func updatePresentationStyle() { navigationItem.rightBarButtonItem?.image = selectedStyle.buttonImage } @objc private func changeContentLayout() { let allCases = PresentationStyle.allCases guard let index = allCases.firstIndex(of: selectedStyle) else { return } let nextIndex = (index + 1) % allCases.count selectedStyle = allCases[nextIndex] } }
UICollectionViewDelegateFlowLayout协议中描述了有关在集合中显示元素的方法的所有内容。 因此,为了从控制器中删除任何实现并创建独立的可重用元素,我们将为每种类型的显示器创建此协议的单独实现。
但是,有两个细微差别:
- 该协议还描述了单元格选择方法(didSelectItemAt :)
- 所有N种映射方法的某些方法和逻辑都是相同的(在我们的示例中,N = 3)。
因此,我们将创建
CollectionViewSelectableItemDelegate协议,扩展标准的
UICollectionViewDelegateFlowLayout协议,在该协议中,我们定义单元格选择的闭包,并在必要时定义任何其他属性和方法(例如,如果使用不同的类型表示,则返回单元格类型)。 这将解决第一个问题。
protocol CollectionViewSelectableItemDelegate: class, UICollectionViewDelegateFlowLayout { var didSelectItem: ((_ indexPath: IndexPath) -> Void)? { get set } }
为了解决第二个问题-使用逻辑重复,我们将创建具有所有常见逻辑的基类:
class DefaultCollectionViewDelegate: NSObject, CollectionViewSelectableItemDelegate { var didSelectItem: ((_ indexPath: IndexPath) -> Void)? let sectionInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 20.0, right: 16.0) func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { didSelectItem?(indexPath) } func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) { let cell = collectionView.cellForItem(at: indexPath) cell?.backgroundColor = UIColor.clear } func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) { let cell = collectionView.cellForItem(at: indexPath) cell?.backgroundColor = UIColor.white } }
在我们的案例中,一般的逻辑是在选择单元格时调用闭包,并在切换到
突出显示状态时更改单元格的背景。
接下来,我们描述表示的3种实现:表格,每行3个元素以及前两种方法的组合。
表格 :
class TabledContentCollectionViewDelegate: DefaultCollectionViewDelegate {
每行3个元素: class DefaultGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8
表格和连续3x的组合。 class CustomGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8
最后一步是将视图数据添加到控制器,并将所需的委托设置为集合。
重要的一点:由于集合的委托是
弱的 ,因此您必须在控制器中与视图对象建立
牢固的链接。
在控制器中,创建有关该类型的所有可用视图的字典:
private var styleDelegates: [PresentationStyle: CollectionViewSelectableItemDelegate] = { let result: [PresentationStyle: CollectionViewSelectableItemDelegate] = [ .table: TabledContentCollectionViewDelegate(), .defaultGrid: DefaultGriddedContentCollectionViewDelegate(), .customGrid: CustomGriddedContentCollectionViewDelegate(), ] result.values.forEach { $0.didSelectItem = { _ in print("Item selected") } } return result }()
然后在
updatePresentationStyle()方法中,将动画更改添加到集合委托:
collectionView.delegate = styleDelegates[selectedStyle] collectionView.performBatchUpdates({ collectionView.reloadData() }, completion: nil)
这就是我们的元素从一个视图动态移动到另一个视图所需要的全部:)
因此,我们现在可以按照自己喜欢的任何方式在任何屏幕上显示元素,在显示之间动态切换,最重要的是,代码是独立的,可重用的和可伸缩的。
完整的项目代码。