Using Closures to initialize properties in Swift
Published on: April 8, 2020There are several ways to initialize and configure properties in Swift. In this week's Quick Tip, I would like to briefly highlight the possibility of using closures to initialize complex properties in your structs and classes. You will learn how you can use this approach of initializing properties, and when it's useful. Let's dive in with an example right away:
struct PicturesApi {
private let dataPublisher: URLSession.DataTaskPublisher = {
let url = URL(string: "https://mywebsite.com/pictures")!
return URLSession.shared.dataTaskPublisher(for: url)
}()
}
In this example, I create a URLSession.DataTaskPublisher
object using a closure that is executed immediately when PicturesApi
is instantiated. Even though this way of initializing a property looks very similar to a computed property, it's really more an inline function that runs once to give the property it's initial value. Note some of the key differences between this closure based style of initializing and using a computed property:
- The closure is executed once when
PicturesApi
is initialized. A computed property is computed every time the property is accessed. - A computed property has to be
var
, the property in my example islet
. - You don't put an
=
sign between the type of a computed property and the opening{
. You do need this when initializing a property with a closure. - Note the
()
after the closing}
. The()
execute the closure immediately whenPicturesApi
is initialized. You don't use()
for a computed property.
Using closures to initialize properties can be convenient for several reasons. One of those is shown in my earlier example. You cannot create an instance of URLSession.DataTaskPublisher
without a URL
. However, this URL
is only needed by the data task publisher and nowhere else in my PicturesApi
. I could define the URL
as a private property on PicturesApi
but that would somehow imply that the URL
is relevant to PicturesApi
while it's really not. It's only relevant to the data task that uses the URL
. Using a closure based initialization strategy for my data task publisher allows me to put the URL
close to the only point where I need it.
Tip:
Note that this approach of creating a data task is not something I would recommend for a complex or sophisticated networking layer. I wrote a post about architecting a networking layer a while ago and in general I would recommend that you follow this approach if you want to integrate a proper networking layer in your app.
Another reason to use closure based initialization could be to encapsulate bits of configuration for views. Consider the following example:
class SomeViewController: UIViewController {
let mainStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 16
return stackView
}()
let titleLabel: UILabel = {
let label = UILabel()
label.textColor = .red
return label
}()
}
In this example, the views are configured using a closure instead of configuring them all in viewDidLoad
or some other place. Doing this will make the rest of your code much cleaner because the configuration for your views is close to where the view is defined rather than somewhere else in (hopefully) the same file. If you prefer to put all of your views in a custom view that's loaded in loadView
instead of creating them in the view controller like I have, this approach looks equally nice in my opinion.
Closure based initializers can also be lazy so they can use other properties on the object they are defined on. Consider the following code:
class SomeViewController: UIViewController {
let titleLabel: UILabel = {
let label = UILabel()
label.textColor = .red
return label
}()
let subtitleLabel: UILabel = {
let label = UILabel()
label.textColor = .orange
return label
}()
lazy var headerStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [self.titleLabel, self.subtitleLabel])
stack.axis = .vertical
stack.spacing = 4
return stack
}()
}
By making headerStack
lazy and closure based, it's possible to initialize it directly with its arranged subviews and configure it in one go. I really like this approach because it really keeps everything close together in a readable way. If you don't make headerStack
lazy, the compiler will complain. You can't use properties of self
before self
is fully initialized. And if headerStack
is not lazy, it needs to be initialized to consider self
initialized. But if headerStack
depends on properties of self
to be initialized you run into problems. Making headerStack
lazy solves these problems.
Closure based initialization is a convenient and powerful concept in Swift that I like to use a bunch in my projects. Keep in mind though, like every language features this feature can be overused. When used carefully, closures can really help clean up your code, and group logic together where possible. If you have any feedback or questions about this article, reach out on Twitter. I love to hear from you.