iTranslated by AI
[Swift] Effective Use of Protocol Extensions
I am keeping a note here of my investigation into the internal implementation of a Swift library called Instantiate.
About Instantiate
Instantiate (GitHub) implements convenient protocols that handle things nicely when you want to perform DI (Dependency Injection) for classes that ViewController subclasses depend on.
Reference: Explanation of DI (Dependency Injection) - Qiita
For example, let's say there is a situation where a ViewController needs to depend on a ViewModel.
let storyboard = UIStoryboard(name: "ViewController", bundle: Bundle.main)
let vc = storyboard.instantiateInitialViewController() as! ViewController
vc.inject(ViewModel())
As shown above, you could declare the ViewController and then call the ViewModel via some method (in this case, the inject method), but this allows for human errors such as forgetting to call the method.
By using Instantiate, you can guarantee the injection at the time of initialization as follows:
let vc = ViewController(with: ViewModel())
I see, that's convenient!
It looks quite concise, but how is this implemented?
Injectable Protocol
First, regarding how to set the class you want to depend on (the ViewModel in the example above) into the ViewController, you simply make it conform to a specific protocol.
If you are building the ViewController's UI with a Storyboard, use StoryboardInstantiatable; if it's a Nib, use NibInstantiatable.
Also, InstantiateStandard automatically detects files with the same class name and handles them appropriately, so it's good to import it.
import Instantiate
import InstantiateStandard
class ViewController: UIViewController {
}
extension ViewController: StoryboardInstantiatable {
func inject(_ dependency: ViewModel) {
viewModel = dependency
}
}
This StoryboardInstantiatable inherits from Injectable.
Furthermore, during ViewController initialization, it internally generates the ViewController from the Storyboard and calls the inject method for you.
And this inject method is pre-implemented as a protocol extension.
(Simplified for clarity)
Injectable.swift
protocol Injectable {
associatedtype Dependency = Void
func inject(_ dependency: Dependency)
}
extension Injectable where Dependency == Void {
func inject(_ dependency: Dependency) {
}
}
Storyboard+UIViewController.swift
protocol StoryboardInstantiatable: Injectable, OtherProtocols {
...
}
The original Injectable has Dependency = Void, and the type is already specified.
This raises the question: if used as is, wouldn't it be impossible to specify types like ViewModel, resulting in Void being used instead?
extension ViewController: StoryboardInstantiatable {
func inject(_ dependency: Void) {
}
}
Trying to create a protocol
You might deepen your understanding by preparing a Playground and actually trying it out while reading.
To follow the specifications of this protocol, the first thing to understand is associatedtype.
First, let's define a protocol using associatedtype for Dependency as shown below. (*1)
Additionally, each VC can define it as a typealias and implement it as a concrete type.
protocol AProtocol {
associatedtype Dependency
func call(dependency: Dependency)
}
extension AProtocol where Self: UIViewController {
func call(dependency: Dependency) {
print("AProtocol: \(dependency)")
}
}
class VoidVC: UIViewController, AProtocol {
typealias Dependency = Void
}
VoidVC().call(dependency: ()) // AProtocol: ()
class StringVC: UIViewController, AProtocol {
typealias Dependency = String
}
StringVC().call(dependency: "aaaaa") // AProtocol: aaaa
Now, let's try defining it within the protocol itself while specifying a particular type. (*2)
We can see that it becomes omittable in the class that inherits from the protocol (VoidVC in this case).
On the other hand, in StringVC, it can be overwritten as Dependency = String.
protocol AProtocol {
associatedtype Dependency = Void
func call(dependency: Dependency)
}
class VoidVC: UIViewController, AProtocol {}
class StringVC: UIViewController, AProtocol {
typealias Dependency = String
}
...
In the above, calling the call method from either VC yields the same result as (*1).
Now, let's implement the call method.
class StringVC: UIViewController, AProtocol {
typealiase Dependency = String
func call(dependency: Dependency) {
print("StringVC: \(dependency)")
}
}
StringVC().call(with: "bbbbb") // StringVC: bbbbb
You can also write it in a shorthand form as follows.
class StringVC: UIViewController, AProtocol {
func call(dependency: String) {
print("StringVC: \(dependency)")
}
}
If you look closely at the argument of the call method, it looks different between Dependency and String.
As some of you might have noticed, by defining a concrete type for the call method's dependency, the compiler automatically interprets it as typealiase Dependency = String and allows the compilation to succeed.
This is the mechanism that allows various types, such as ViewModels, to be injected as dependencies.
Summary
In this article, we looked at the internal implementation using the library Instantiate as a case study.
- Usage of
associatedtypeandtypealias - About
protocol extension
I hope this helps your understanding of the topics above at least a little bit.
It is very helpful to see how the Swift language specifications are used so effectively. 👏
Investigating how OSS is implemented like this is a great learning experience, so I hope to continue doing it.
Thank you for reading to the end!
Discussion