Here I chose to define Style as an associatedtype because I didn't want to couple it directly to UIFont.TextStyle. Instead, I defined another protocol inheriting from Font.
protocol ScalableFont: Font where Style == UIFont.TextStyle {
// fun stuff...
}
extension ScalableFont {
func scaledFont(forStyle style: Style, compatibleWith traitCollection: UITraitCollection? = nil) -> UIFont {
let metrics = UIFontMetrics(forTextStyle: style)
let base = font(forStyle: style)
// some fun stuff...
}
}
I then wrote a test function that let me preview my font in a UIStackView.
func previewFont(_ font: ScalableFont) {
// more fun stuff...
}
But... hello darkness, my old friend:
Protocol 'ScalableFont' can only be used as a generic constraint because it has Self or associated type requirements
Personal note: I loath this error. On many occasions it's stopped me from truly embracing protocols in more situations and shunted me towards generic structs/classes which I don't find as expressive or extendable.
I'm baffled by this error because the compiler has been told that Style is equal toUIFont.TextStyle. I apologise ahead of time if this is covered in the Generics Manifesto, I've read it through twice since it was published but I'm clearly still missing something.
Can anyone shed some light on this real world example?
That is not correct. StyledFont is only another protocol that refines your Font protocol, and it is constrained so that conforming types must have the exact same Style type as the constrain says. The StyledFont protocol can still be upcasted to Font which will remove the constraint and then you‘ll end up with the same error again.
I'm not trying to be difficult, I understand all those words but I still don't quite understand why I can't use ScalableFont as a parameter type without changing the function declaration to this:
Is it only because of upcasting? Wouldn't the place where I try and upcast it to Font be the more appropriate place to show this compile time error?
Maybe I'm showing my naïveté but previewFont(_ font: ScalableFont) knows all of the types involved. The value font is a value with all of the functions of Font and ScaledFont but only those where Style is equal to UIFont.TextStyle.
Hmm... I might have typed my way to an understanding.
Is the difference between the plain (and invalid) previewFont(_:) and the generic previewFont<T: ScalableFont>(_:) the moment when it needs to call font(forStyle:) and it doesn't know the underlying type of font so it doesn't know which implementation to reach for?
Here's some code to help me reason about this.
extension ScalableFont {
func font(forStyle style: Style) -> UIFont {
return UIFont.preferredFont(forTextStyle: style)
}
}
struct AvenirNext: ScalableFont {
func font(forStyle style: Style) -> UIFont {
let font = //... some black magic here
return font
}
}
struct System: ScalableFont {}
let f = AvenirNext()
previewFont(f) // if previewFont(_ font: ScalableFont) compiled
Then from inside previewFont(_:) it doesn't know if it should reach for the default implementation, AvenirNext's implementation, or System's (non-existent) implementation of font(forStyle:). Whereas the generic version is kind of like passing in a promise of functionality (aka a protocol) and where to go find the implementations?
The actual problem here (IIRC) is that, currently, the compiler does not actually check whether or not all associated types are fully constrained, only whether or not the protocol or its ancestors have declared an associatedtype (or used Self).
This second constraint is easier to implement, and works for most cases, but does prevent some cases that seem like they should work, like yours.