Type Erasure in Swift
How to deal with generic and associated types?
In my last post, I wrote about splitting up code into smaller frameworks. But sometimes this is easier said than done. My current challenge is to modulize reusable UI-handling. That’s why I created FancyUI. I implemented a component-manager for themes support additionally to some configuration extensions.
Styleables and the Component-Manager in FancyUI
Styleables are views that inherit the Styleable protocol. They enforce you to implement the style(colorScheme:)
method with that you can configure colors of your views. You can register them with the component-manager. The component-manager acts as an observer while the styleables itself are listeners to theme changes. You can easily pass new Color-Schemes via the component-manager to easily manage theme support in your App.
The Problem
Implementations of FancyUI should be able to create custom ColorScheme types. Therefore Styleable has to use a generic type for ColorScheme. Styleable is a protocol so ColorScheme has to be an associated type. The component-manager (which is a struct) holds the ColorScheme and passes it to the registered styleables. And here’s the problem: Because of the component-manager being struct, it has the generic-type GenericSchemeType
whereas the Styleable has an associated type SchemeType
. I must convert the generic SchemeType into an associated type SchemeType
which is not possible in swift.
The solution: Type Erasure
The problem is that the type can’t be inferred at compile time. That’s why I attempted to create a struct AnyStyleable. This type stores the style function as a closure and executes it when the style()
function gets called. The component-manager now registers AnyStyleable instead of Styleable.
Note: The type-information isn’t really erased. It’s rather relocated. The closure infers its type during runtime so we don’t have a problem here.
The problem is fixed. But I have an architectural problem with the solution: The styling type has to be saved as closure and the view isn’t the Styleable itself. As I mentioned already, I love extracting stuff. I imagine using the style method in a large, grown enterprise App. Setting the AnyStyleable in viewDidLoad or init would be bloating stuff up so my initial intention to the Styleable protocol was to implement it in separate extensions (and let the view-controller be the Styleable itself). This isn’t possible anymore because the styling is located into another type. But I also got a solution to this problem.
Alternative approach: Real type erasure
In my current approach, the original Styleableprotocol hasn’t much changed. It’s nearly the same except that the protocol is inherited by another protocol: AnyStyleable.Where AnyStyleable was a struct which took the styling-function as a closure, it’s now a protocol which defines the style(scheme:)
method as an Any type. That means: AnyStyleable is a protocol with a concrete type (even it’s Any). That allows us to register AnyStyleables to the component-manager. And here comes the magic: As I mentioned, the Styleable protocol inherits the AnyStyleable protocol. Therefore I can register Styleables to the component manager because it awaits types of AnyStyleable.
You might ask yourself now: Styleables have the style(colorScheme:)
method. How can they be called when they’re actually registered as AnyStyleable?
The AnyStyleable protocol defines the _style(colorScheme:)
method which is implemented as a default implementation. The _style(colorScheme:)
implementation checks if the passed type matches the associated type during runtime and if successful, calls the style(colorScheme)
function. So actually the component-manager calls the _style(colorScheme:)
method which delegates to the style(colorScheme:)
method during runtime.
The benefit of this approach is that the implementation has not to deal with the erasure at all. It can just call register(self)
as used and the implemented style function can be extracted to extensions.
Conclusion
I generally try to avoid type-checks and casts. But this approach seems legit to me (also because it’s used for some types in the standard library). However, if you have a cleaner solution or improvements feel free to reach out. I’m always willing to improve my quality.
You can check out my code here: FancyUI