implementing UIScrollViewDelegate in a UICollectionView subclass

Viewed 1056

I have a UICollectionView subclass.

I want to add a default implementation for scrollViewDidScroll from UIScrollViewDelegate

Is there a way to access the scrollView delegate methods from the UICollectionView subclass?

Thanks

2 Answers

You can add a function that implement the default code you need, for example:

class YourCollectionView: UICollectionView {

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func updateUIOnScrollViewDidScroll(_ scrollView: UIScrollView) {
        //...
    }
}

Then when you implement the delegate functions in your view controller, add:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    yourCollectionView.updateUIOnScrollViewDidScroll(scrollView)
}

EDIT

If you want to use your collection like an external library and you don’t want to call the updated function every time, you can implement a custom class that only conform to the UICollectionViewDelegate (if you want you can have a separated CustomDataSource class too that implement data source and delegate), for example:

class YourCollectionViewDelegate: NSObject, UICollectionViewDelegate {
    // implement a callback for every function you need to manage in the view controller 
    var onSelectedItemAt: ((IndexPath) -> Void)?
    func collectionView(_ collectionView: UICollectionView, 
         didSelectItemAt indexPath: IndexPath) 
        onSelectedItemAt?(indexPath)
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
     guard let collectionView = scrollView as? YourCollectionViewClass else { fatalError(“your message”) }
    // implement your ui update 
}

Then in your view controller you just have to bind the delegate with your view controller:

class MyViewController: UIViewController {

    //...
    let customDelegate = YourCollectionViewDelegate()

    override func viewDidLoad() {
         super.viewDidLoad()
        //...
        myCollection.delegate = customDelegate
        setupBindings()
    }

    private func setupBindings() {

        customDelegate.onSelectedItemAt = { [weak self] indexPath in 
            //...
        }
    }

We can use delegation concept to get access from CustomCollectionView whenever user scrolls it.

This is the implementation of CustomCollectionView also note that the delegate method is optional which are available in Objective C.

So that, if your ViewController confirms to the CustomCollectionViewDelegate protocol then it needs to implement the delegate methods else it have not to.

Note:

However, CustomCollectionView is a subclass of UICollectionView means its simple generic UI element. In fact, it is a View in Model-View-Controller (MVC). As per MVC, View can not be directly talk to the Controller, the communication between View and Controller is blind & structured. The good examples of such kind of communication are Target & Action and delegate pattern.

The delegate is a simple variable which is contained by the generic UI components like UIScrollView, UITableView, UICollectionView, etc. The Controller has to confirms the protocol by setting up the delegate of the UI element to self to implement the delegate methods inside it.

The conclusion is the subclass of generic UI element can't implement the delegate methods inside it.

However, we can achieve that by making a customized UIView with XIB and add the collectionView to it.

Code:

CustomCollectionView:

import UIKit

@objc protocol CustomCollectionViewDelegate {

    @objc optional func collectionViewDidScroll(_ scrollView: UIScrollView)
}

class CustomCollectionView: UIView {

    //MARK: - Outlets
    @IBOutlet weak var collection: UICollectionView!

    //MARK: - Variables
    weak var vc: UIViewController!
    weak var view: UIView!
    weak var customDelegate: CustomCollectionViewDelegate?

    let titles = ["HorizontalCollectionView", "VerticalCollectionView"]

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    init(frame: CGRect, in vc: UIViewController, setCustomDelegate set: Bool) {
        super.init(frame: frame)
        xibSetup(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
        self.vc = vc
        self.customDelegate = set ? vc as? CustomCollectionViewDelegate : nil
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        xibSetup(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
    }

    private func xibSetup(frame: CGRect) {

        view = loadViewFromNib()
        view.frame = frame
        addSubview(view)

        collection.register(UINib(nibName: "CustomCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "CustomCollectionViewCell")
        collection.delegate = self
        collection.dataSource = self
    }

    private func loadViewFromNib() -> UIView {

        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: "CustomCollectionView", bundle: bundle)
        let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView

        return view
    }

}

extension CustomCollectionView: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        if customDelegate != nil {
            customDelegate!.collectionViewDidScroll!(scrollView)
        }
    }
}

extension CustomCollectionView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

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

        return titles.count
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {

        return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {

        return 0
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {

        return 10 // Adjust the inter item space based on the requirement.
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        return CGSize(width: 300, height: collectionView.bounds.height)
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCollectionViewCell", for: indexPath) as! CustomCollectionViewCell
        cell.titleLabel.text = titles[indexPath.row]
        return cell
    }
}

CustomCollectionView XIB:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina4_7" orientation="portrait">
        <adaptation id="fullscreen"/>
    </device>
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <objects>
        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CustomCollectionView" customModule="SampleDemoApp" customModuleProvider="target">
            <connections>
                <outlet property="collection" destination="loF-CI-n5C" id="EZi-It-39z"/>
            </connections>
        </placeholder>
        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="CustomCollectionView" customModule="SampleDemoApp" customModuleProvider="target">
            <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
            <subviews>
                <collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="loF-CI-n5C">
                    <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                    <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                    <collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="10" id="HQB-uW-7CY">
                        <size key="itemSize" width="50" height="50"/>
                        <size key="headerReferenceSize" width="0.0" height="0.0"/>
                        <size key="footerReferenceSize" width="0.0" height="0.0"/>
                        <inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
                    </collectionViewFlowLayout>
                </collectionView>
            </subviews>
            <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            <constraints>
                <constraint firstItem="loF-CI-n5C" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="Khs-Aw-6b7"/>
                <constraint firstItem="loF-CI-n5C" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="cEr-al-Pib"/>
                <constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="loF-CI-n5C" secondAttribute="bottom" id="ftp-QG-OGJ"/>
                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="loF-CI-n5C" secondAttribute="trailing" id="num-9n-spN"/>
            </constraints>
            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
            <point key="canvasLocation" x="138.40000000000001" y="153.37331334332833"/>
        </view>
    </objects>
</document>

ViewController:

override func viewDidLoad() {
    super.viewDidLoad()

    let collectionView = CustomCollectionView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 200), in: self, setCustomDelegate: true)
    view.addSubview(collectionView)
}

Delegate Implementation:

extension ViewController: CustomCollectionViewDelegate {

    func collectionViewDidScroll(_ scrollView: UIScrollView) {

        //do something...
    }
}
Related