Swift 中 UITableViewCell 中的同步滚动 UICollectionViews

Posted

技术标签:

【中文标题】Swift 中 UITableViewCell 中的同步滚动 UICollectionViews【英文标题】:Synchronised Scrolling UICollectionViews in UITableViewCell in Swift 【发布时间】:2018-01-26 12:48:48 【问题描述】:

我的结构是这样的:

UITableView -> UITableViewCell -> UICollectionView -> UICollectionViewCell

所以我想要实现的是我想让 UITableViewCells 中的 UICollectionViews 滚动同步。例如,当您手动滚动第一行的第一个 UICollectionView 时,我希望 UICollectionViews 的其余部分跟随,但文本标签始终保持在同一位置. (请看下图)

编辑:我知道我必须以某种方式使用contentOffset,但不知道如何在这种情况下实现。任何帮助将不胜感激。

Click to see the image

Click to see the gif

【问题讨论】:

很抱歉,目前尚不清楚您想要实现什么。能否请您张贴您想要的 GIF,或者至少是一个更好的解释? @GiuseppeLanza 通过添加 gif 来编辑问题。谢谢! 所以如果一个内部集合视图滚动,所有其他的必须滚动相同。对吗? @GiuseppeLanza 完全正确。 由于UICollectionViewUITableViewUIScrollView 的子类,您可以使用UIScrollViewDelegate 来检测scrollViewDidScroll: 中特定UICollectionView 的滚动(可能在@ 987654330@)。之后,您只需将相同的contentOffset 应用于您的集合引用的每个集合视图。 【参考方案1】:

好的,我设法让它工作,请记住,代码仅用于提问目的,包含许多非泛型参数和强制转换,应不惜一切代价避免。

包含 tableView 的 MainViewController 类:

    protocol TheDelegate: class 
    func didScroll(to position: CGFloat)


    class ViewController: UIViewController, TheDelegate 

        func didScroll(to position: CGFloat) 
            for cell in tableView.visibleCells as! [TableViewCell] 
                (cell.collectionView as UIScrollView).contentOffset.x = position
            
        

        @IBOutlet var tableView: UITableView!

        override func viewDidLoad() 
            super.viewDidLoad()
            tableView.dataSource = self
        
    

    extension ViewController: UITableViewDataSource 
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
            return 100
        

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as? TableViewCell else  return UITableViewCell() 
            cell.scrollDelegate = self
            return cell
        
    

tableViewCell 的类:

class TableViewCell: UITableViewCell 

    @IBOutlet var collectionView: UICollectionView!

    weak var scrollDelegate: TheDelegate?

    override func awakeFromNib() 
        super.awakeFromNib()
        (collectionView as UIScrollView).delegate = self
        collectionView.dataSource = self
    


extension TableViewCell: UICollectionViewDataSource 

    func numberOfSections(in collectionView: UICollectionView) -> Int 
        return 1
    

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return 100
    

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as! CollectionViewCell
        cell.imageView.image = #imageLiteral(resourceName: "litecoin.png")
        return cell
    


extension TableViewCell: UIScrollViewDelegate 

    func scrollViewDidScroll(_ scrollView: UIScrollView) 
        scrollDelegate?.didScroll(to: scrollView.contentOffset.x)
    

collectionViewCell 的类是无关紧要的,因为它只是实现细节。稍后我会将此解决方案发布到 github。

免责声明:这仅适用于可见单元格。您还需要为准备重用的单元格实现当前滚动状态。我会在github上扩展代码。

【讨论】:

【参考方案2】:

我想出了一个可以在操场上测试的可行解决方案:

//: A UIKit based Playground for presenting user interface

import UIKit
import PlaygroundSupport

class MyCollectionCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegate 
  var originatingChange: Bool = false
  var observationToken: NSKeyValueObservation!
  var offsetSynchroniser: OffsetSynchroniser? 
    didSet 
      guard let offsetSynchroniser = offsetSynchroniser else  return 
      collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)

      observationToken = offsetSynchroniser.observe(\.currentOffset)  (_, _) in
        guard !self.originatingChange else  return 
        self.collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)
      
    
  

  lazy var collection: UICollectionView = 
    let layout = UICollectionViewFlowLayout()
    layout.itemSize = CGSize(width: 40, height: 40)
    layout.scrollDirection = .horizontal
    let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
    collection.backgroundColor = .white
    collection.dataSource = self
    collection.delegate = self
    collection.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")

    return collection
  ()

  override func layoutSubviews() 
    super.layoutSubviews()
    collection.frame = contentView.bounds
    contentView.addSubview(collection)
  

  func numberOfSections(in collectionView: UICollectionView) -> Int 
    return 1
  

  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
    return 10
  

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
    cell.layer.borderColor = UIColor.black.cgColor
    cell.layer.borderWidth = 1
    cell.backgroundColor = .white

    return cell
  

  func scrollViewDidScroll(_ scrollView: UIScrollView) 
    originatingChange = true
    offsetSynchroniser?.currentOffset = scrollView.contentOffset
    originatingChange = false
  


class OffsetSynchroniser: NSObject 
  @objc dynamic var currentOffset: CGPoint = .zero


class MyViewController : UIViewController, UITableViewDataSource 
  var tableView: UITableView!
  let offsetSynchroniser = OffsetSynchroniser()

  override func loadView() 
    let view = UIView()
    view.backgroundColor = .white

    tableView = UITableView(frame: .zero, style: .plain)

    tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    view.addSubview(tableView)
    tableView.dataSource = self

    tableView.register(MyCollectionCell.self, forCellReuseIdentifier: "cell")

    self.view = view
  

  override func viewDidLoad() 
    super.viewDidLoad()
    tableView.reloadData()
  

  func numberOfSections(in tableView: UITableView) -> Int 
    return 1
  

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
    return 10
  

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCollectionCell
    cell.selectionStyle = .none
    cell.collection.reloadData()
    cell.offsetSynchroniser = offsetSynchroniser
    return cell
  

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

要使其与 Playground 一起使用,您会看到很多代码,如果您使用的是故事板或 xib,则不需要这些代码。无论如何,我希望基本思想是明确的。

说明

基本上我创建了一个名为OffsetSynchroniser 的对象,它有一个名为currentOffset 的可观察属性。 tableView 的每个单元格都接受一个 offsetSynchroniser,并且在 didSet 上它们向 KVO 注册以获取 currentOffset 更改的通知。

每个单元格还注册到自己的集合的委托并实现 didScroll 委托方法。

当任何这些 collectionView 导致此方法被触发时,同步器的 currentOffset var 会发生变化,并且通过 KVO 订阅的所有单元格都会对这些变化做出反应。

Observable 对象非常简单:

class OffsetSynchroniser: NSObject 
  @objc dynamic var currentOffset: CGPoint = .zero

那么您的 tableViewCell 将拥有此对象类型的实例,并且在 didSet 上将使用 KVO 注册到 var currentOffset:

  var originatingChange: Bool = false
  var observationToken: NSKeyValueObservation!
  var offsetSynchroniser: OffsetSynchroniser? 
    didSet 
      guard let offsetSynchroniser = offsetSynchroniser else  return 
      collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)

      observationToken = offsetSynchroniser.observe(\.currentOffset)  (_, _) in
        guard !self.originatingChange else  return 
        self.collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)
      
    
  

originatingChange 变量是为了避免实际启动偏移量更改的 collectionView 会通过导致偏移量被重新设置两次来做出反应。

最后,始终在您的 TableViewCell 中,将自身注册为 collectionViewDelegate 后,您将实现 didScroll 的方法

  func scrollViewDidScroll(_ scrollView: UIScrollView) 
    originatingChange = true
    offsetSynchroniser?.currentOffset = scrollView.contentOffset
    originatingChange = false
  

在这里我们可以改变同步器的currentOffset。

此时 tableViewController 将拥有同步器的所有权

 class YourTableViewController: UItableViewController  // or whatever ViewController contains an UITableView
      let offsetSynchroniser = OffsetSynchroniser()
      ...
      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
          let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCollectionCell
          ...
          cell.offsetSynchroniser = offsetSynchroniser
          return cell
      
 

【讨论】:

很好地使用了 keyValueObserving 和 + 来使它更通用一点! 我编辑了答案以详细解释解决方案 它仅适用于可见单元格。请提供任何可用的解决方案...【参考方案3】:

我能想到的最好的方法就是将你所有的collectionViews 存储在一个collection 对象中。然后,您可以从这些 collectionView 中使用 UIScrollViewscrollViewDidScroll 委托方法。只需确保您的委托设置正确即可。

func scrollViewDidScroll(_ scrollView: UIScrollView) 
    for view in collectionViewCollection where view.scrollView != scrollView
        view.scrollView.contentOffset = scrollView.contentOffset
    

这是未经测试的代码,因此不是完整的答案,但它应该可以帮助您入门。

【讨论】:

感谢您的回复!

以上是关于Swift 中 UITableViewCell 中的同步滚动 UICollectionViews的主要内容,如果未能解决你的问题,请参考以下文章

Swift 中 UITableViewCell 中的同步滚动 UICollectionViews

在嵌入在 UITableviewCell 中的 UICollectionView 中进行 Segue,Swift

swift中选择后UITableViewCell布局错误

Swift交替UITableViewCell渐变颜色

关闭tableviewcontroller中uitableviewcell中文本字段的键盘 - Swift

Swift 中 UITableViewCell 中的 UIScrollView