围绕头部的UICollectionView:即时更改视图

哈Ha! 我向您展示了文章“ UICollectionView教程:动态更改演示文稿 ”的翻译。

在本文中,我们将考虑使用各种显示元素的方式,以及它们的重用和动态变化。 在这里,我们将不讨论使用集合和自动布局的基础知识。

结果,我们得到一个例子:


在开发移动应用程序时,通常情况下,表视图不够用,您需要显示更加有趣和独特的元素列表。 此外,更改元素显示方式的功能可以成为应用程序中的“芯片”。

使用UICollectionView和UICollectionViewDelegateFlowLayout协议的各种实现,上述所有功能都很容易实现。

完整的项目代码。

首先我们需要实现:

  • 类FruitsViewController:UICollectionViewController。
  • 水果数据模型

    struct Fruit { let name: String let icon: UIImage } 
  • 类FruitCollectionViewCell:UICollectionViewCell

带有UIImageView和UILabel的单元格用于显示水果


我们将使用xib在一个单独的文件中创建单元以供重用。

通过设计,我们看到有2个可能的单元格选项-图像下面和文本右边。



单元格的类型可能完全不同,在这种情况下,您需要创建2个单独的类并使用所需的类。 在我们的例子中,没有这种需要,使用UIStackView的1个单元就足够了。



为单元创建接口的步骤:

  1. 添加UIView
  2. 在其中添加UIStackView(水平)
  3. 接下来,将UIImageView和UILabel添加到UIStackView。
  4. 对于UILabel,为水平和垂直设置“内容压缩抗性优先级”的值= 1000。
  5. 为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] } } // MARK: UICollectionViewDataSource & UICollectionViewDelegate extension FruitsViewController { override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return datasource.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FruitCollectionViewCell.reuseID, for: indexPath) as? FruitCollectionViewCell else { fatalError("Wrong cell") } let fruit = datasource[indexPath.item] cell.update(title: fruit.name, image: fruit.icon) return cell } } 

UICollectionViewDelegateFlowLayout协议中描述了有关在集合中显示元素的方法的所有内容。 因此,为了从控制器中删除任何实现并创建独立的可重用元素,我们将为每种类型的显示器创建此协议的单独实现。

但是,有两个细微差别:

  1. 该协议还描述了单元格选择方法(didSelectItemAt :)
  2. 所有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 { // MARK: - UICollectionViewDelegateFlowLayout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let paddingSpace = sectionInsets.left + sectionInsets.right let widthPerItem = collectionView.bounds.width - paddingSpace return CGSize(width: widthPerItem, height: 112) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return sectionInsets } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 10 } } 

每行3个元素:

 class DefaultGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8 // MARK: - UICollectionViewDelegateFlowLayout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let paddingSpace = sectionInsets.left + sectionInsets.right + minimumItemSpacing * (itemsPerRow - 1) let availableWidth = collectionView.bounds.width - paddingSpace let widthPerItem = availableWidth / itemsPerRow return CGSize(width: widthPerItem, height: widthPerItem) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return sectionInsets } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 20 } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return minimumItemSpacing } } 

表格和连续3x的组合。

 class CustomGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate { private let itemsPerRow: CGFloat = 3 private let minimumItemSpacing: CGFloat = 8 // MARK: - UICollectionViewDelegateFlowLayout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let itemSize: CGSize if indexPath.item % 4 == 0 { let itemWidth = collectionView.bounds.width - (sectionInsets.left + sectionInsets.right) itemSize = CGSize(width: itemWidth, height: 112) } else { let paddingSpace = sectionInsets.left + sectionInsets.right + minimumItemSpacing * (itemsPerRow - 1) let availableWidth = collectionView.bounds.width - paddingSpace let widthPerItem = availableWidth / itemsPerRow itemSize = CGSize(width: widthPerItem, height: widthPerItem) } return itemSize } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return sectionInsets } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 20 } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return minimumItemSpacing } } 

最后一步是将视图数据添加到控制器,并将所需的委托设置为集合。

重要的一点:由于集合的委托是弱的 ,因此您必须在控制器中与视图对象建立牢固的链接。

在控制器中,创建有关该类型的所有可用视图的字典:

 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) 

这就是我们的元素从一个视图动态移动到另一个视图所需要的全部:)


因此,我们现在可以按照自己喜欢的任何方式在任何屏幕上显示元素,在显示之间动态切换,最重要的是,代码是独立的,可重用的和可伸缩的。

完整的项目代码。

Source: https://habr.com/ru/post/zh-CN445708/


All Articles