How to Override ScrollViewDidEndDecelerating and find current visible cell ? - ios

I have a view controller which contains a collectionView:
class TagViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let post = posts[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SNPostViewCell
cell.isVideo = post.isVideo
cell.mediaURL = URL(string: post.mediaURL)
cell.backgroundColor = UIColor.black
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let post = posts[indexPath.row]
if post.isVideo == true {
self.performSegue(withIdentifier: "playVideo", sender: nil)
} else {
print("is image. might go full screen one day here")
}
let bin = self.bins[post.timeStamp]
print(bin)
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { //Error: method cannot override from its superclass
<#code#>
}
}
Although I can override those collectionView functions, it seems that to override scrollViewDidEndDecelerating I specifically need to have an instance of collectionView (and not do it in a view controller)
OK so I made a custom collectionView which inherits from collectionViewController. This overrides scrollViewDidEndDecelerating correctly (the method now DOES override from its superclass), and I am using code inside of it to find the index of the current cell,
class PagingCollectionViewController: UICollectionViewController {
...
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if (self.collectionView.contentOffset.y < 0) {
self.collectionView.contentOffset = CGPointMake(self.collectionView.contentOffset.x, 0.0);
}
for cell in yourCollectionViewname.visibleCells() as [UICollectionViewCell] {
let indexPath = yourCollectionViewname.indexPathForCell(cell as UICollectionViewCell)
}
}
...
}
However, as you can see this needs to reference the current index of the collectionView which I wouldn't have defined in the class right? Only in the view Controller where the instance is created and populated...
So I don't really understand where to put this override scrollViewDidEndDecelerating function and how to access the data within it successfully - to find the index.

This is wrong:
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
<#code#>
}
... because scrollViewDidEndDecelerating is not an override method. It is a delegate method.
https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619417-scrollviewdidenddecelerating
So delete the word override, and set self as the delegate of the collectionView, and you're all set.

Related

UICollectionView subclass with internal delegate doesn't get triggered when external delegate hasn't implemented the same method

I'm working on a 3rd party framework for swift so i cannot use the delegate methods of UICollectionViewDelegate myself but I do need them for some custom logic.
Tried multiple approaches to make it work, including method swizzling but in the end I felt like it was too hacky for what i'm doing.
Now i'm subclassing UICollectionView and setting the delegate to an internal (my) delegate.
This works well except for when the UIViewController hasn't implemented the method.
right now my code looks like this:
fileprivate class UICollectionViewDelegateInternal: NSObject, UICollectionViewDelegate {
var userDelegate: UICollectionViewDelegate?
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || userDelegate?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if userDelegate?.responds(to: aSelector) == true {
return userDelegate
}
return super.forwardingTarget(for: aSelector)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didEnd(item: indexPath.item)
userDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
}
}
class CustomCollectionView: UICollectionView {
private let internalDelegate: UICollectionViewDelegateInternal = UICollectionViewDelegateInternal()
required init?(coder: NSCoder) {
super.init(coder: coder)
super.delegate = internalDelegate
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
super.delegate = internalDelegate
}
func didEnd(item: Int) {
print("internal - didEndDisplaying: \(item)")
}
override var delegate: UICollectionViewDelegate? {
get {
return internalDelegate.userDelegate
}
set {
self.internalDelegate.userDelegate = newValue
super.delegate = nil
super.delegate = self.internalDelegate
}
}
}
In the ViewController I just have a simple set up with the delegate method for didEndDisplaying not implemented
Is it possible to listen to didEndDisplaying without the ViewController having it implemented?
Edit 1:
Here's the code of the ViewController to make it a little more clear what i'm doing
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CustomCollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .blue
return cell
}
// func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// print("view controller - did end displaying: \(indexPath.item)")
// }
}
the didEndDisplaying of CustomCollectionView is only triggered when i uncomment the didEndDisplaying method in the ViewController.
what i'm looking for is to have the didEndDisplaying of CustomCollectionView also triggered if the didEndDisplaying method in the ViewController is NOT implemented.
hope it's a little more clear now
Edit 2:
Figured out that the code above had some mistakes which made the reproduction not work as I intended. updated the code above.
also made a github page to make it easier to reproduce here:
https://github.com/mees-vdb/InternalCollectionView-Delegate
I did a little reading-up on this approach, and it seems like it should work - but, obviously, it doesn't.
Played around a little bit, and this might be a solution for you.
I made very few changes to your existing code (grabbed it from GitHub - if you want to add me as a Collaborator I can push a new branch [same DonMag ID on GitHub]).
First, I implemented didSelectItemAt to make it a little easier to debug (only one event at a time).
ViewController class
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CustomCollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .blue
return cell
}
// DonMag - comment / un-comment these methods
// to see the difference
//func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// print("view controller - did end displaying: \(indexPath.item)")
//}
//func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// print("view controller - didSelectItemAt", indexPath)
//}
}
UICollectionViewDelegateInternal class
fileprivate class UICollectionViewDelegateInternal: NSObject, UICollectionViewDelegate {
var userDelegate: UICollectionViewDelegate?
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || userDelegate?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if userDelegate?.responds(to: aSelector) == true {
return userDelegate
}
return super.forwardingTarget(for: aSelector)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didEnd(item: indexPath.item)
userDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didSel(p: indexPath)
userDelegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
}
}
CustomCollectionView class
// DonMag - conform to UICollectionViewDelegate
class CustomCollectionView: UICollectionView, UICollectionViewDelegate {
private let internalDelegate: UICollectionViewDelegateInternal = UICollectionViewDelegateInternal()
required init?(coder: NSCoder) {
super.init(coder: coder)
super.delegate = internalDelegate
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
super.delegate = internalDelegate
}
func didEnd(item: Int) {
print("internal - didEndDisplaying: \(item)")
}
func didSel(p: IndexPath) {
print("internal - didSelectItemAt", p)
}
// DonMag - these will NEVER be called,
// whether or not they're implemented in
// UICollectionViewDelegateInternal and/or ViewController
// but, when implemented here,
// it allows (enables?) them to be called in UICollectionViewDelegateInternal
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
print("CustomCollectionView - didEndDisplaying", indexPath)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("CustomCollectionView - didSelectItemAt", indexPath)
}
override var delegate: UICollectionViewDelegate? {
get {
// DonMag - return self instead of internalDelegate.userDelegate
return self
//return internalDelegate.userDelegate
}
set {
self.internalDelegate.userDelegate = newValue
super.delegate = nil
super.delegate = self.internalDelegate
}
}
}

Hint on how to create & pass data via segue when collectionview cell is pressed

I have a UICollectionView embedded in a UITableViewCell & I want to perform a segue when a UICollectionViewCell is pressed & pass the data represented by that cell to the destination ViewController for detailed information.
Here is the code for the embedded UICollectionView inside the UITableViewCell
#IBOutlet weak var EventCollection: UICollectionView!
var events = [Events]()
override func awakeFromNib() {
super.awakeFromNib()
EventCollection.delegate = self
EventCollection.dataSource = self
}
extension PopularCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return events.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = EventCollection.dequeueReusableCell(withReuseIdentifier: "EventCell", for: indexPath) as! EventCell
let event = events[indexPath.row]
print("Event Name:\(event.event_name)")
cell.event = event
cell.tag = indexPath.row
return cell
}
How do I perform & prepare a segue in the main ViewController when a UICollectionViewCell is pressed so as to pass the data contained by that cell to the destination ViewController
Here are the steps that you need to do.
As you said, your CollectionView is inside TableView. So your TableView delegates/DataSources binded with the MainViewController. CollectionView delegates/DataSources binded with the TableViewCell.
Now create a protocol to know user has clicked on collectionView.
protocol MyProtocol : class {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
}
In TableViewCell, you need to call delegate like this,
class MyTableCell : UITableViewCell, UICollectionViewDelegate {
weak var delegate : MyProtocol?
:
:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let delegate = delegate {
delegate.collectionView(collectionView, didSelectItemAt: indexPath)
}
}
}
Now your MainViewController must have to conform this protocol,
class MainViewController :UIViewController, MyProtocol {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Do segue here ....
}
}
Note : Make sure to bind delegate with your MainViewController i.e. in TableviewCellForRow have cell.delegate = self
Add the following code with in a new file named UIView_Ext
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
In func didSelectItem(At indexPath: IndexPath) method , write the following code
self.parentViewController?.performSegue(withIdentifier: "Identifer", sender: "Your Data in place of this string")

Nested Controllers using UICollectionViewController reordering

I have a UIViewController with a UICollectionViewController nested inside. The collection view controller also has the moveItemAt method implemented, since I want the cells to be reorderable. So the cells have a UILongPressGestureRecognizer attached. However, long press on the cells aren't happening. I can't seem to figure out if the nested controller is causing the gesture to be ignored. Maybe the parent controller is capturing the long press but that wouldn't make sense since AFAIK, gestures go up the view hierarchy, not the other way around.
For some context, I've used this method to nest my controllers
func add(_ child: UIViewController) {
addChildViewController(child)
child.view.frame = view.bounds
view.addSubview(child.view)
child.didMove(toParentViewController: self)
}
I think this is a better approach...
Assuming you have a UICollectionViewController in your storyboard, and you've assigned the cell prototype to DragMeCell and set its Identifier to "DragMeCell" (and added a label connected to the IBOutlet), this should run and allow you to long-press drag-drop to reorder.
//
// DragReorderCollectionViewController.swift
// SW4Temp
//
// Created by Don Mag on 7/25/18.
//
import UIKit
private let reuseIdentifier = "DragMeCell"
class DragMeCell: UICollectionViewCell {
#IBOutlet var theLabel: UILabel!
}
class DragReorderCollectionViewController: UICollectionViewController {
fileprivate var longPressGesture: UILongPressGestureRecognizer!
fileprivate var dataArray = Array(0 ..< 25)
override func viewDidLoad() {
super.viewDidLoad()
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
if let cv = self.collectionView {
cv.addGestureRecognizer(longPressGesture)
}
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
if let cv = self.collectionView,
let gestureView = gesture.view {
switch(gesture.state) {
case .began:
guard let selectedIndexPath = cv.indexPathForItem(at: gesture.location(in: cv)) else {
break
}
cv.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
cv.updateInteractiveMovementTargetPosition(gesture.location(in: gestureView))
case .ended:
cv.endInteractiveMovement()
default:
cv.cancelInteractiveMovement()
}
}
}
// MARK: UICollectionViewDelegate
override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let i = dataArray[sourceIndexPath.item]
dataArray.remove(at: sourceIndexPath.item)
dataArray.insert(i, at: destinationIndexPath.item)
}
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArray.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! DragMeCell
// Configure the cell
cell.theLabel.text = "\(dataArray[indexPath.item])"
return cell
}
}
Then you can add it as a child view controller, using the code snippet you posted in your question.

Make sure only 1 cell has an active state in a UICollectionView

I have an UICollectionView in which I want only want 1 cell to be active. With active I mean: the last cell that has been clicked (or the very first cell when to collection view lays out). When a user clicks a non-active cell, I want to reset the old active cell to a non-active state. I am having trouble doing this. This is because visibleCells, a property of collection view, only returns the cells on screen but not the cells in memory. This is my current way to locate an active cell and reset the state to non active.
This scenario can happen, causing multiple active cells: A user scroll slightly down so that the current active cell is not visible anymore, taps on a random cell and scroll up. The problem is that the old active cell stays in memory, although it is not visible: cellForItemAt(_:) does not gets called for that cell. Bad news is that visibleCells also do not find the old active cell. How can I find it? The function willDisplay cell also does not work.
An example project can be cloned directly into xCode: https://github.com/Jasperav/CollectionViewActiveIndex.
This is the code in the example project:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CollectionView!
static var activeIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
collectionView.go()
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var button: MyButton!
}
class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
func go() {
delegate = self
dataSource = self
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 500
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
if indexPath.row == ViewController.activeIndex {
cell.button.setTitle("active", for: .normal)
} else {
cell.button.setTitle("not active", for: .normal)
}
cell.button.addTarget(self, action: #selector(touchUpInside(_:)), for: .touchUpInside)
return cell
}
#objc private func touchUpInside(_ sender: UIButton){
let hitPoint = sender.convert(CGPoint.zero, to: self)
guard let indexPath = indexPathForItem(at: hitPoint), let cell = cellForItem(at: indexPath) as? Cell else { return }
// This is the problem. It does not finds the current active cell
// if it is just out of bounds. Because it is in memory, cellForItemAt: does not gets called
if let oldCell = (visibleCells as! [Cell]).first(where: { $0.button.titleLabel!.text == "active" }) {
oldCell.button.setTitle("not active", for: .normal)
}
cell.button.setTitle("active", for: .normal)
ViewController.activeIndex = indexPath.row
}
}
To recover from this glitch you can try in cellForRowAt
cell.button.tag = indexPath.row
when the button is clicked set
ViewController.activeIndex = sender.tag
self.reloadData()
You can use the isSelected property of the UIColectionViewCell. You can set an active layout to your cell if it is selected. The selection mechanism is implemented by default in the UIColectionViewCell. If you want to select/activate more than one cell you can set the property allowsMultipleSelection to true.
Basically this approach will look like this:
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.go()
}
func activeIndex()->Int?{
if let selectedItems = self.collectionView.indexPathsForSelectedItems {
if selectedItems.count > 0{
return selectedItems[0].row
}
}
return nil
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var myLabel: UILabel!
override var isSelected: Bool{
didSet{
if self.isSelected
{
myLabel.text = "active"
}
else
{
myLabel.text = "not active"
}
}
}
}
class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
func go() {
delegate = self
dataSource = self
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 500
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
return cell
}
}

Synchronised Scrolling UICollectionViews in UITableViewCell in Swift

I have the structure like this:
UITableView -> UITableViewCell -> UICollectionView ->
UICollectionViewCell
So what I’m trying to achieve is that I want to make UICollectionViews in UITableViewCells to scroll synchronised. For example when you scroll manually the first UICollectionView on the first row, I want the rest of UICollectionViews to follow, but the Text Labels to stay in the same position all the time. (Please see the image below)
EDIT: I know that I have to use contentOffset somehow, but don’t know how to implement in this case scenario. Any help would be appreciated.
Click to see the image
Click to see the gif
Okay I managed to get this working, Please keep in mind the code is just for the question purposes and contains lot of non-generic parameters and force casting that should be avoided at any cost.
The class for MainViewController containing the tableView:
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
}
}
The class for your 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)
}
}
The class for the collectionViewCell is irelevant since it's just implementation detail. I will post this solution to github in a second.
Disclaimer: This works just for visible cells. You need to implement the current scroll state for the cells ready for reuse as well. I will extend the code on github.
I came up with a working solution you can test on a playground:
//: 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()
To make it work with a playground you will see a lot of code that if you are using storyboards or xib is not needed. I hope anyway that the base idea is clear.
Explanation
Basically I created an object called OffsetSynchroniser which has an observable property called currentOffset. Each cell of the tableView accepts an offsetSynchroniser and on didSet they register with KVO for notifications of currentOffset changes.
Each cells also registers to its own collection's delegate and implements the didScroll delegate method.
When any of those collectionView causes this method to be triggered the currentOffset var of the synchroniser is changed and all the cells that are subscribed through KVO will react to the changes.
The Observable object is very simple:
class OffsetSynchroniser: NSObject {
#objc dynamic var currentOffset: CGPoint = .zero
}
then your tableViewCell will have an instance of this object type and on didSet will register with KVO to the 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)
}
}
}
The originatingChange variable is to avoid that the collectionView that is actually initiating the offset change will react by causing the offset to be re-set twice.
Finally, always in your TableViewCell, after registering itself as collectionViewDelegate you will implement the method for didScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
originatingChange = true
offsetSynchroniser?.currentOffset = scrollView.contentOffset
originatingChange = false
}
In here we can change the currentOffset of the synchroniser.
The tableViewController will at this point just have the ownership for the synchroniser
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
}
}
The best way I can think of off the top of my head to do something like this would be to store all of your collectionViews in a collection object. You can then use the UIScrollView's scrollViewDidScroll delegate method from those collectionViews. Just make sure you have your delegate set correctly.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for view in collectionViewCollection where view.scrollView != scrollView{
view.scrollView.contentOffset = scrollView.contentOffset
}
}
This is untested code, so not a complete answer but it should get you started.

Resources