iosswiftswift-protocolsassociated-typesdefault-implementation

Trouble with Swift Protocols, associatedtypes, Self and default implementations


I am trying to get some functionality through default implementations that I can't nail. Consider the following code, which is a simplification of what I'm trying to do, but captures the problem as simply as possible.

//protocol definition
protocol Configurable {
    associatedtype Data
    func configure(data: Data)

    static func generateObject() -> Self
}

//default implementation for any UIView
extension Configurable where Self: UIView {
    static func generateObject() -> Self {
        return Self()
    }
}

//implement protocol for UILabels
extension UILabel: Configurable {
    typealias Data = Int

    func configure(data: Int) {
        label.text = "\(data)"
    }
}

//use the protocol
let label = UILabel.generateObject()
label.configure(data: 5)
print(label.text!)  //5

I have a protocol, a default implementation for some methods for UIView, and the a specific implementation for UILabel.

My issue is the last part... the actual use of all this functionality

let label = UILabel.generateObject()
label.configure(data: 5)
print(label.text!)  //5

I find myself doing generateObject() followed by configure(data: <something>) constantly. So I tried doing the following:

Add static func generateObjectAndConfigure(data: Data) -> Self to the protocol. The issue comes when I try to make a default implementation for UIView for this method. I get the following error

Method 'generateObjectAndConfigure(data:)' in non-final class 'UILabel' cannot be implemented in a protocol extension because it returnsSelfand has associated type requirements

Basically, I can't have a method that returns Self and uses an associated type. It feels really nasty for me to always call the two methods in a row. I want to only declare configure(Data) for each class and get generateObjectAndConfigure(Data) for free.

Any suggestions?


Solution

  • You're overcomplicating this a bit, by using Self.

    All you need to do is declare an initialiser in your Configurable protocol that accepts your Data associatedtype as an argument, and has a non-static configure function:

    protocol Configurable {
        associatedtype Data
        init(data: Data)
        func configure(data: Data)
    }
    

    Provide a default implementation of that initializer in an extension for the Configurable protocol (for UIView and its subclasses):

    extension Configurable where Self: UIView {
        init(data: Data) {
            self.init(frame: CGRect.zero)
            self.configure(data: data)
        }
    }
    

    Finally, add conformance to the protocol via an extension to any UIView subclasses you're interested in. All you need to do here is to implement the typealias and configure method:

    extension UILabel: Configurable {
    typealias Data = Int
    func configure(data: Data) {
        text = "\(data)"
    }
    

    }

    extension UIImageView: Configurable {
        typealias Data = String
        func configure(data: Data) {
            image = UIImage(named: data)
        }
    }
    

    This implementation has the added bonus that you're using an initializer to create your views (the standard Swift pattern for instantiating an object), rather than a static method:

    let label = UILabel(data: 10)
    let imageView = UIImageView(data: "screenshot")
    

    It's not exactly clear to me why the compiler doesn't like your version. I would have thought that subclasses of UILabel would inherit the typealias meaning that the compiler shouldn't have a problem inferring both Self and Data, but apparently this isn't supported yet.

    Edit: @Cristik makes a good point about UICollectionView in the comments.

    This problem can be solved by adding a protocol extension for Configurable where the Self is UICollectionView, using the appropriate initializer:

    extension Configurable where Self: UICollectionView {
        init(data: Data) {
            self.init(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
            self.configure(data: data)
        }
    }
    

    Then, when adding conformance to Configurable for UICollectionView, we make the Data typealias a UICollectionViewLayout:

    extension UICollectionView: Configurable {
        typealias Data = UICollectionViewLayout
        func configure(data: Data) {
            collectionViewLayout = data
        }
    }
    

    Personally, I think this is a reasonable approach for classes where the init(frame:) initializer isn't appropriate.