Reputation: 1492
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
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)
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))
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
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
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