【问题标题】:Synchronised Scrolling UICollectionViews in UITableViewCell in SwiftSwift 中 UITableViewCell 中的同步滚动 UICollectionViews
【发布时间】: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 应用于您的集合引用的每个集合视图。

标签: ios swift xcode uitableview uicollectionview


【解决方案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
        }
    }
    

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

    【讨论】:

    • 感谢您的回复!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-01
    相关资源
    最近更新 更多