Friday, September 9, 2022

Using Lazy Properties for Views

Steve Landey:

There are three patterns I use in most of my UIKit projects that I’ve never seen anyone else talk about. I think they help readability a lot, so I’m sharing them here[…]

[…]

Additionally, it’s not great to use force-unwrapped optionals to store anything. But if we use let instead, then all views will be created at init time instead of in loadView().

[…]

We can solve a lot of problems by moving all view creation to the bottom of the file and using lazy var.

The standard guidance is to use implicitly unwrapped optionals to reference views in a view controller, but I think lazy works much better. Aside from the delayed loading benefit, lazy lets you reference self, and thus other properties and helper methods, which you would not ordinarily be able to do from a Swift initializer. (The compiler does protect you from making circular references to other lazy properties.) And, because you are providing an initial value, the type of the property can be inferred.

A downside is that there’s no lazy let, so the compiler can’t prevent you from accidentally mutating a variable that you meant to only set once. One way around this is to use a helper object with all the properties as let and to store that in the var, but I think the awkwardness this causes is worse than the problem it solves. Landey suggests:

You can at least prevent multiple writes to vars at runtime by using this simple property wrapper, which throws an assertion failure in debug builds if you write to the property more than once. It’s not as good as a compile-time guarantee, but it does double as inline documentation.

You can decide whether the protection is worth the noise. I’m more interested in something like this to prevent write access to conceptually immutable properties except from tests. However, that is more relevant for model properties, which often use Core Data, and property wrappers don’t work with @NSManaged. Core Data itself provides a—more verbose—out, though: you can still set a property with a private setter by key.

Landey then recommends defining the properties at the top of the file and using helper functions at the bottom of the file to actually create the views. Personally, I prefer to see everything related to a property together at its declaration. If you use lazy and create the initial value right there, you can set up the whole property without having to repeat its name or type anywhere.

Previously:

4 Comments RSS · Twitter

Most of those view properties should be private already, but if they aren't make sure at least they are private(set)

private gets in the way of testing; I wish it worked with @testable. private(set) is good if you don’t mind the verbosity. It’s too bad Swift doesn’t let you mark whole sections as having the same access like you can in Objective-C and C++.

I solve the problem of messy loadView by using a UIView subclass for each view controller. So you’ll have XViewController accompanied by XView. All XViewController needs to do in loadView is self.view = XView() and hook up any actions etc, and let the view create the hierarchy in its init. You get nice let bindings for the subviews you need in the view class and separate the noise of layout constraints from the view controller logic.

But that addSubviews method is a brilliant idea.

@Juri The XView subclass is essentially what I was thinking about with the “helper object.” It can be a view but doesn't have to be. The issue I ran into there is that either you let the subclass set up the views, which means passing info down into its initializer, or you do some setup in the subclass and some in loadView(). Either way, it felt to me like duplication that wasn’t doing much for me. Maybe I just didn’t figure out quite the right way of doing it.

I have a separate method in each view controller that sets up the hierarchy, mostly using vStack() and hStack() methods that are kind of like the suggested addSubviews().

Leave a Comment