sliwinski.lukas
sliwinski.lukas

Reputation: 1492

How to call subclass method from superclass with protocol conformance

I'm creating swift UI component that will provide some functionality to the UIView and UICollectionViewCell. I want to make it easy to customise. (code here are of course a simplification) In code below I will do some hard work in layoutSubview (I have to override layoutSubviews for each custom class because in extension we can't override class methods) and then call the add method with default implementation that should be easy to change its behaviour if needed.

Problem is that creating SubClassView instance calls correctly CustomView.layoutSubviews but inside this method SomeProtocol.add from extension is called instead of SubClassView.add. I understand that this is by Swift design but how can achieve that developer of my component without overriding layoutSubviews that I prepared for him, overrides only that one add method prepared by me for customisation with default implementation that is ready for him if he is not interested in changing it.

protocol SomeProtocol: class {
    var property: Int { get set }
    func add() // Method that can be override for custom behaviour
}

extension SomeProtocol where Self: UIView {
    // Default implementation
    func add() {
        property += Int(frame.width)
        print("SomeProtocol.add [property: \(property)]")
    }
}

class CustomView: UIView, SomeProtocol {
    var property: Int = 1

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomView.layoutSubviews")
        // Some specific high end code for CustomView
        add()
    }
}

class CustomCollectionViewCell: UICollectionViewCell, SomeProtocol {
    var property: Int = 2

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomCollectionViewCell.layoutSubviews")
        // Some specific high end code for CustomCollectionViewCell
        add()
    }

}

class SubClassView: CustomView { // Class that can implemented by the third party developer for customisation
    // This method will not be called
    func add() {
        property += 10
        print("SubClassView.add [property: \(property)]")
    }
}

let view = SubClassView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
// prints:
// CustomView.layoutSubviews
// SomeProtocol.add [property: 101]

Upvotes: 3

Views: 4626

Answers (3)

sliwinski.lukas
sliwinski.lukas

Reputation: 1492

I play with this and I came up with 2 different approaches. (github with playground -> https://github.com/nonameplum/Swift-POP-with-Inheritance-problem)

  1. Composition
struct BaseFunctionality<T: UIView where T: SomeProtocol> {
    var add: (T) -> Void
    init() {
        add = { (view) in
            view.property += Int(view.frame.width)
            print("BaseFunctionality [property: \(view.property)]")
        }
    }
}

protocol SomeProtocol: class {
    var property: Int { get set }
}

class CustomView: UIView, SomeProtocol {
    var property: Int = 1
    var baseFunc = BaseFunctionality<CustomView>()

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomView.layoutSubviews")
        // Some specific high end code for CustomView
        baseFunc.add(self)
    }
}

class CustomCollectionViewCell: UICollectionViewCell, SomeProtocol {
    var property: Int = 2
    var baseFunc = BaseFunctionality<CustomCollectionViewCell>()

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomCollectionViewCell.layoutSubviews")
        // Some specific high end code for CustomCollectionViewCell
        baseFunc.add(self)
    }

}

class SubClassView: CustomView { // Class that can implemented by the third party developer for customisation

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.baseFunc.add = { (view) in
            view.property -= Int(view.frame.width)
            print("SubClassView [property: \(view.property)]")
        }
    }

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

}

let view = SubClassView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
  1. Kind of delegates
protocol Functionality: class {
    func add()
}

// Default implementation
extension SomeProtocol where Self: UIView {
    func add() {
        property = -5
        print("Functionality.add [property = \(property)]")
    }
}

protocol SomeProtocol: class {
    var property: Int { get set }
    var delegate: Functionality? { get set }
}

class CustomView: UIView, SomeProtocol {
    var property: Int = 1
    weak var delegate: Functionality?

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomView.layoutSubviews")
        // Some specific high end code for CustomView
        if let delegate = delegate {
            delegate.add()
        } else {
            self.add()
        }
    }
}

class CustomCollectionViewCell: UICollectionViewCell, SomeProtocol {
    var property: Int = 2
    weak var delegate: Functionality?

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomCollectionViewCell.layoutSubviews")
        // Some specific high end code for CustomCollectionViewCell
        if let delegate = delegate {
            delegate.add()
        } else {
            self.add()
        }
    }
}

class SubClassView: CustomView { // Class that can implemented by the third party developer for customisation
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.delegate = self
    }

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

extension SubClassView: Functionality {
    func add() {
        property = 5
        print("SubClassView.add [property = \(property)]")
    }
    func doNothing() { print("SubClassView.doNothing [\(property)]") }
}

let view = SubClassView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))

Upvotes: 0

Rob Napier
Rob Napier

Reputation: 299265

First, let's move all the animation stuff into its own thing.

struct CustomAnimator {
    var touchesBegan: (UIView) -> Void = { $0.backgroundColor = .red() }
    var touchesEnded: (UIView) -> Void = { $0.backgroundColor = .clear() }
}

So this is how we want to animate things. When touches begin, we'll make them red. When touches end, we'll make them clear. This is just an example, and of course can be overridden; these are just default values. This may not be the most convenient data structure for you; you could come up with lots of other ways to store this. But the key is that it's just a struct that animates the thing it is passed. It's not a view itself; there's no subclassing. It's just a value.

So, how can we use this? In cases where it's convenient to subclass, we can use it this way:

class CustomDownButton: UIButton {
    var customAnimator: CustomAnimator?

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        customAnimator?.touchesBegan(self)
        super.touchesBegan(touches, with: event)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        customAnimator?.touchesEnded(self)
        super.touchesEnded(touches, with: event)
    }
}

So if you feel like subclassing your view, you can just assign a CustomAnimator onto it and it'll do the thing. But that's not really the interesting case, IMO. The interesting case is when it's not convenient to subclass. We want to attach this to a normal button.

UIKit already gives us a tool for attaching touch behavior to random views without subclassing them. It's called a gesture recognizer. So let's build one that does our animations:

class CustomAnimatorGestureRecognizer: UIGestureRecognizer {
    let customAnimator: CustomAnimator

    init(customAnimator: CustomAnimator) {
        self.customAnimator = customAnimator
        super.init(target: nil, action: nil)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        if let view = view {
            customAnimator.touchesBegan(view)
        }
        super.touchesBegan(touches, with: event)
    }

    override func reset() {
        // I'm a bit surprised this isn't in the default impl.
        if self.state == .possible {
            self.state = .failed
        }
        super.reset()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        if let view = view {
            customAnimator.touchesEnded(view)
        }
        self.reset()
        super.touchesEnded(touches, with: event)
    }
}

It should be pretty obvious how this works. It just calls the animator to modify our view any time touches start or stop. There's a little bit of bookkeeping needed to properly handle the state machine, but it's very straightforward.

There is no step three. Setting this up is trivial:

override func viewDidLoad() {
    super.viewDidLoad()
    customButton.customAnimator = CustomAnimator()
    normalButton.addGestureRecognizer(CustomAnimatorGestureRecognizer(customAnimator: CustomAnimator()))
}

Full project on GitHub.

Upvotes: 0

kamil b
kamil b

Reputation: 43

You can add indirect class between yours CustomView and SomeProtocol, lets name it CustomViewBase. This class should inherit UIView and implement SomeProtocol. Now make your class CustomView subclass of CustomViewBase and add to it implementation of add() method and call from it super.add().

Here is code:

protocol SomeProtocol: class {
    var property: Int { get set }
    func add() // Method that can be override for custom behaviour
}

extension SomeProtocol where Self: UIView {
    // Default implementation
    func add() {
        property += Int(frame.width)
        print("SomeProtocol.add [property: \(property)]")
    }
}

class CustomViewBase: UIView, SomeProtocol {
    var property: Int = 1
}

class CustomView: CustomViewBase {
    func add() {
        super.add()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomView.layoutSubviews 2")
        // Some specific high end code for CustomView
        add()
   }
}

class CustomCollectionViewCell: UICollectionViewCell, SomeProtocol {
    var property: Int = 2

    override func layoutSubviews() {
        super.layoutSubviews()
        print("CustomCollectionViewCell.layoutSubviews")
        // Some specific high end code for CustomCollectionViewCell
        add()
    }    
}

class SubClassView: CustomView { // Class that can implemented by the     third party developer for customisation
    // This method will not be called
    override func add() {
        property += 10
        print("SubClassView.add [property: \(property)]")
    }
}

let view = SubClassView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))

Upvotes: 1

Related Questions