Swift generics in view controllers with @dynamicMemberLookup + keypaths

In this article, I’d like to discuss a way of setting a custom UIView to the main view of a UIViewController and how generics, @dynamicMemberLookup with Key Path Member Lookup can make our life easier reusing those custom views.

Without Generics (Base classes) 🧐

Let’s assume we need to reuse a custom view (UIView subclass) in multiple view controllers, but for some reason, we don’t have generics for that purpose.

One way is to create a superclass view controller and set the instance of our CustomView in the loadView function, as follows:

class BaseViewController: UIViewController {  lazy var someView = CustomView()   override func loadView() {
self.view = someView
}
}

The next step is to implement a specific logic in the subclass:

class ViewControllerA: BaseViewController {override func viewDidLoad(){
super.viewDidLoad()
// some specific logic of view controller a
}
}class ViewControllerB: BaseViewController { override func viewDidLoad(){
super.viewDidLoad()
// some specific logic of view controller b
}
}

That could be a nice and simple solution if we would have to reuse only CustomView, but in reality, we might have more than one reusable custom view in our project.

Let’s say we have a new AdditionalCustomView and it needs to be reused as well, but since our BaseViewController has someView with a hardcoded type of CustomView, we have no choice but to write additional superclass where someView type is AdditionalCustomView.

That leaves us with many view controller classes that are doing basically the same thing, but with a different view.

So what can we do?

We can use generics of course!

With using generics 🤓

//1
class GenericFormVC<V: UIView>: UIViewController {

lazy var genericView = V()
//2
override func loadView() {
self.view = genericView
}
}
  1. GenericFormVC has a generic type <V: UIView> indicating that the type must be UIView or its subclass.
  2. In loadView() we set the instance self.view as we did previously, but this time we set it withgenricViewof type V.

Reusable View Example (FormView)

Assuming we have a view named FormView :

class FormView: UIView {  lazy var firstLabel = UILabel()
lazy var
secondLabel = UILabel()
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
// UI layout
}
}

And a FormViewController subclass to make it more specific and customizable:


class FormViewController: GenericFormVC<FormView> {
override func viewDidLoad() {
super.viewDidLoad()
self.genericView.firstLabel.text = "First name"
self.genericView.secondLabel.text = "Last name"
}
}

FormViewController inherits from GenericFormVC with a specific type of FormView, at this point the compiler can infer the type of genericView, that will give us type safety when accessing the properties of FormView e.g: firstLabel.

That way of writing view controllers gives us a lot of flexibility since we can add many types of reusable views, but having only one GenericFormVC that adds them as self.view .

Some syntactic sugar with Key Path Member Lookup (for Swift 5.1 or higher)🍩🍫🍭

As you’ve noticed, the access to the properties of genericView is a bit too long:

self.genericView.firstLabel.text = “First name”

It would be nice if we could access it as if it was a property of our view controller:

self.firstLabel.text = “First name”

For that purpose, we can use @dynamicMemberLookup(SE-0195 introduced in Swift 4.2) with Key Path Member Lookup (SE-0252 introduced in Swift 5.1) features:

@dynamicMemberLookup
class GenericFormVC<V: UIView>: UIViewController {

lazy var genericView: V = V()
override func loadView() {
self.view = genericView
}
subscript<T>(dynamicMember keyPath: KeyPath<V,T>) -> T {
return genericView[keyPath: keyPath]
}
}

By annotating GenericFormVC class with @dynamicMemberLookup and implementing subscript<T>(dynamicMember keyPath: KeyPath<V,T>) -> Twe can access the internal, public or open properties ofgenericView in our subclasses, as if they are part of our view controller:

class FormViewController: GenericFormVC<FormView> {
override func viewDidLoad() {
super.viewDidLoad()
self.firstLabel.text = "First name"
self.secondLabel.text = "Last name"

}
}

Summary:

Although it might be a bit tricky to understand the magic self.firstLabel.text = "First name" if you are not familiar with @dynamicMemberLookup, keypaths and generics, we can achieve code reusability, type safety, better build times (since we have fewer classes to build) and modernise our UIKit code with Swift’s latest APIs.

Please don’t hesitate to write your feedback, thoughts it in the comments 🙂.