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 returns
Selfand 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?
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.