Using Closures to initialize properties in Swift

Published by donnywals on

There 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 is let.
  • 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 when PicturesApi 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.


Learn Combine with my new book

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get eleven chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $19.99!

Get Practical Combine

Receive weekly updates about my posts

Categories: Quick Tip