Handling optional in SwiftUI view

Tue, 30 Mar 2021 00:00:00 +0000

Let's have a look to how handling optional content in SwiftUI without relying on heavy `if let` everywhere in our codebase.

Last version of Swift brought many improvements to @ResultBuilder. Yet one thing I still find myself doing often is checking whether my data is nil to pass it to a View like Text. Well today I'd like to share with you a small tip I found about SwiftUI and optionals.

Failable initializer to the rescue!

Some time ago I wanted to refactor some code to make my view code more concise and easier to read:

// move from this...
struct ContentView: View {
  var body: some View {
    if let name = myParticipant.name {
      Text(logicToComputeName(name))
        .font(.bold)
    }
  }
}

// ...to this
struct ContentView: View {
  var body: some View {
    Text(name: myParticipant)
      .font(.bold)
  }
}

So I started to write a custom Text.init:

extension Text {
  init(name participant: Participant) {
    if let name = participant.name {
      self.init(logicToComputeName(name))
    }
    else {
        // what should I do here??
    }
  }
}

But as you can see data might be nil. In that case no Text should be created. How to handle that? Well failable initializer seemed like the way to go. But would it actually work and compile with SwiftUI/ResultBuilder?

extension Text {
  init?(name participant: Participant) {
    guard let name = participant.name else {
      return nil
    }

    Text(participant.name)
  }
}

// Testing with this code
struct ContentView: View {
  var body: some View {
    Text(name: myParticipant)
      .font(.bold)
  }
}

Well... it actually did! But something was strange because I didn't had to unwrap my view in order to use modification functions (.font and so on) 🤔. Only reason I could see is that Optional is probably a View which would explain why we can call modifiers without unwrapping.

To be sure let's try to implementing View on Optional:

extension Optional: View where Wrapped: View { }

You should then see a message from the compiler stating that "Conformance of 'Optional<Wrapped>' to protocol 'View' was already stated in the protocol's module 'SwiftUI'". So Optional is indeed a View.

So we can actually make our SwiftUI View init as failable initializer to optional/empty cases!

For instance we could add a generic failable initializer to Text:

extension Text {
  @_disfavoredOverload
  init?<S: StringProtocol>(_ content: S?) {
    guard let content = content else {
      return nil
    }

    self.init(content)
  }
}

Which make obviously less if let in my view codebase 🎉.

Should we go down that way though? If Apple did not provide Text with such a init there might be (maybe) a reason. While pretty convenient it is true that in the meantime it is less clear the view might not be displayed.

In the end I guess it's mostly a developer choice. On my side I'm using failable initializers only when making custom init and leaved generic ones out of code.