loading...

On if let bindings in SwiftUI

marionauta profile image Mario Nachbaur ・2 min read

Recently I was building an app that lists public libraries in my home city. In early stages, the main view was just a list view that displays the library name and address. Something like this:

Screenshot showing a quick prototype for my app

Quick prototype of my app

As you can see, some libraries don't have an address, because of some unrelated reasons. And if the address isn't present, the name should be vertically centred in the row.

A library not having an address is translated into my data model as an optional String. Here's a simplified version of my Library model:

struct Library {
    let id: String
    let name: String
    let address: String?
}
Neat, eh?

Initial approach

If a library doesn't have an address, I won't render a Text component for it, simple as that. And the easiest way for me to do that was using an if let binding:

VStack {
    Text(library.name)
    if let address = library.address {
        Text(address)
    }
}
This WON'T compile!

As it turns out, SwiftUI doesn't like this. The compiler tells us that Closure containing control flow statement cannot be used with function builder 'ViewBuilder'. I beg your pardon?

How else am I supposed to conditionally render views in my app if "control flow statement cannot be used" to build it? After a moment of panic, I tested using a normal if:

VStack {
    Text(library.name)
    if library.address != nil {
        Text(library.address!)
    }
}
Force unwrapping make kittens cry

And surprisingly that worked... wtf. Needless to say, I wasn't very exited with that solution, since it requires force unwrapping an optional value. I knew that there was another way.

Current solution

One of the key features of SwiftUI is that if we add a nil component to our view, it won't show up, it's just ignored. So, what's the easiest way to convert a conditional string into a conditional Text? That's right, using map.

Here we map over the possibly nil address, without needing to force unwrap it, as the closure will only be executed if there was a value in the first place. This, however, seems like a hack. But hey, It works.

VStack {
    Text(library.name)
    library.address.map { address in
        Text(address)
    }
}
Heh, mapping an optional... I can still hear my CS professor talking about monads.

Let's wait and see

As I was reading some discussions about this issue, I found out that SwiftUI builds views using a struct named ViewBuilder — remember the error I was getting before? If we peek its definition (cmd + shift + o in XCode to go to any class definition) we can clearly see that it supports if statements, but nothing more.

SwiftUI is not event a year old, and we know Apple only introduces big features once a year. So here's hoping that when the next version of SwiftUI shows up, they add if let support.

Discussion

markdown guide
 

if let – is from another paradigm.
The sense of SwiftUI goes to declarative approach, but not into imperative, which is classic.

 

If you're saying that if let isn't supported in SwiftUI because it's not declarative, then why is the normal if supported? It's even used in the official documentation. To me, both share the same level of "imperativeness", but the if is not as type-safe in this case!

It's clear that SwiftUI it's inspired by other declarative frameworks like React.js or Vue.js, and in those is idiomatic to check if values are present to conditionally render a view. Something like this:

library.address !== undefined && <span>{library.address}</span>

Here, since we are checking that library.address has a value, TypeScript smart casts it from a string? to a normal string. It's the TS equivalent of an if let binding.

 

I wouldn't use React as example of correctness, as it's also not a mature technology.
In case of SwiftUI, I think, they will propose appropriate usage for cases that you aware of.