1

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

swiftyMees
  • 33
  • 6
  • It's a little confusing what you're asking because you say *"In the ViewController I just have a simple set up with the delegate method for didEndDisplaying not implemented"* ... but you don't show your view controller code (and there's not enough in what you posted to copy/paste and run). If you can put together a [mre] (just enough to show what you're trying to do, and what's not working), I *may* have a solution. – DonMag Oct 25 '22 at 18:23
  • I updated my question, I hope this helps a little to make the situation more clear! thanks in advance. – swiftyMees Oct 26 '22 at 06:55

2 Answers2

3

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
        }
    }
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • This completely solved my issue, thank you so much! this is great :) please push a new branch, I added you as a Collaborator! – swiftyMees Nov 18 '22 at 14:50
  • @swiftyMees - pushed my changes on new branch – DonMag Nov 18 '22 at 15:07
  • @DonMag its super-weird that if you just subclass a UICollectionView (don't have an inner delegate) and have the call `override func numberOfItems(inSection` (sic) it DOES call that seemingly each time? the data is reloaded (and you have to super. to make it call the normal delegate upstream). HOWEVER, the similar override `override func cellForItem(at` unfortunately seems to "not work", it ignores the override and it's never called. – Fattie Apr 27 '23 at 15:11
0

Here's a somewhat cleaner way, depending on your tastes, to intercept the delegates in a UIViewController:

Part 1, I do it the "other" way around. The natural delegate is your delegate and you keep track of the "outer" delegate(s).

In this example I intercept both delegate and dataSource, but I think you could as needed intercept only one or the other.

class TrickCollection: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
    
    weak var outsideDelegate: UICollectionViewDelegate?
    weak var outsideDataSource: UICollectionViewDataSource?
    
    override var delegate: UICollectionViewDelegate? {
        
        set { outsideDelegate = newValue }
        get { return outsideDelegate }
    }
    
    override var dataSource: UICollectionViewDataSource? {
        
        set { outsideDataSource = newValue }
        get { return outsideDataSource }
    }
    

Part 2, in bringup, hijack the delegate(s).

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        common()
    }
    
    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        common()
    }
    
    func common() {
        super.delegate = self
        super.dataSource = self
    }
    

Part 3, you now have total power over the delegates.

However it's important to realize ... see below ...

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        let k = outsideDataSource?.collectionView(self, numberOfItemsInSection: section) ?? 0
        print("yo, i intercepted the count \(k)")
        return k
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = outsideDataSource?.collectionView(self, cellForItemAt: indexPath) as! YourCellClass
        print("yo, i intercepted a cell \(cell.yourData?.headline)")
        return cell
    }

However it's important to realize that this is a "you have total control" setup. Nothing is passed on to the sucker "outer" delegate, unless you want it to be.

For me personally, that's actually sensible and cool. TrickCollection is not a UIViewCollection, it's different. Consuming programmers (likely yourself :) ) shouldn't expect it to behave the same.

Don't forget, philosophically any time of the million times yu've typed something as common as "layoutSubviews" it is your choice whether or not you call super-layoutSubviews, and critically, at what point in your code you do call it.

So for me it's a non-issue, in fact it's correct, that in the approach given here all the delegate calls are NOT available upstream (unless you specifically make it so).

The trade-off result is that the code is trivial and clean.

Note that indeed this is the same as ... https://stackoverflow.com/a/75997746/294884

If this approach is handy for anyone, great!

Regarding collection views / table views. I have found that, in reality, in the real world I've never not had to! intercept the collection/table view in this way. It's inevitable that an app when a collection view is being used, there's "something else" you need to sync with the movement, and you don't wanna clutter up the consumer code upstream. Anyway.

Here's the whole thing as a copy/paste for convenience.

class TrickCollection: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
    
    weak var outsideDelegate: UICollectionViewDelegate?
    weak var outsideDataSource: UICollectionViewDataSource?
    
    override var delegate: UICollectionViewDelegate? {
        
        set { outsideDelegate = newValue }
        get { return outsideDelegate }
    }
    
    override var dataSource: UICollectionViewDataSource? {
        
        set { outsideDataSource = newValue }
        get { return outsideDataSource }
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        common()
    }
    
    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        common()
    }
    
    func common() {
        super.delegate = self
        super.dataSource = self
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        let k = outsideDataSource?.collectionView(self, numberOfItemsInSection: section) ?? 0
        print("yo, i intercepted the count \(k)")
        return k
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = outsideDataSource?.collectionView(self, cellForItemAt: indexPath) as! YourCellClass
        print("yo, i intercepted a cell \(cell.yourData?.headline)")
        return cell
    }
}

Again, you will often just want one or the other of the two delegates.

Fattie
  • 27,874
  • 70
  • 431
  • 719