1

I want a if let statement inside a View.

@ObservedObject var person: Person?
 var body: some View {
      if person != nil {
        // this works
      }
      if let p = person {
        // Compiler error
      }
    }

Closure containing control flow statement cannot be used with function builder 'ViewBuilder'

4
  • 1
    Nope, that's not currently supported. The only kind of control flow you can have in a view builder is if...else. Commented Feb 13, 2020 at 7:55
  • so I should forcecast i those situations? Commented Feb 13, 2020 at 7:56
  • 2
    using force cast is not a good solution. It's better to check with nil and handle it in if-else Commented Feb 13, 2020 at 8:11
  • 1
    At the moment you can either force cast/unwrap or call another property or function that isn't directly in an @ViewBuilder scope so you can use normal control flow like if let. Commented Feb 13, 2020 at 8:12

3 Answers 3

4

2022 Update

You can just use normal Swift if let:

if let pieceOfData = pieceOfData {
  // now it's guaranteed
} else {
  // now it's not
}

Previous Answer

Or you can create your own IfLet view builder:

import SwiftUI

struct IfLet<Value, Content, NilContent>: View where Content: View, NilContent: View {

    let value: Value?
    let contentBuilder: (Value) -> Content
    let nilContentBuilder: () -> NilContent

    init(_ optionalValue: Value?, @ViewBuilder whenPresent contentBuilder: @escaping (Value) -> Content, @ViewBuilder whenNil nilContentBuilder: @escaping () -> NilContent) {
        self.value = optionalValue
        self.contentBuilder = contentBuilder
        self.nilContentBuilder = nilContentBuilder
    }

    var body: some View {
        Group {
            if value != nil {
                contentBuilder(value!)
            } else {
                nilContentBuilder()
            }
        }
    }
}

extension IfLet where NilContent == EmptyView {

    init(_ optionalValue: Value?, @ViewBuilder whenPresent contentBuilder: @escaping (Value) -> Content) {
        self.init(optionalValue, whenPresent: contentBuilder, whenNil: { EmptyView() })
    }
}

Using this, you can now do the following:

var body: some View {
    IfLet(pieceOfData) { realData in
        // realData is no longer optional
    }
}

Want to respond if your optional is nil?

var body: some View {
    IfLet(pieceOfData, whenPresent: { realData in
        // realData is no longer optional
        DataView(realData)
    }, whenNil: {
        EmptyDataView()
    })
}
Sign up to request clarification or add additional context in comments.

1 Comment

Since this is the accepted answer it should be updated to show that if let is now supported
3

Yes, the notation requested in code snapshot is not allowed, at least for now, but the intended result is possible to achieve with the following very simple approach - extract control flow into function:

var person: Person? // actually @ObservedObject does not allowed optional
var body: some View {
    VStack {
         if person != nil {
           // same as before
         }
         personViewIfExists() // << just extract it in helper function
    }
}

private func personViewIfExists() -> some View { // generates view conditionally
    if let p = person {
      return ExistedPersonView(person: p) // << just for demo
    }
}

on some conditions also the following variant of function might be required (although it produces the same result)

private func personViewIfExists() -> some View {
    if let p = person {
      return AnyView(ExistedPersonView(person: p))
    }
    return AnyView(EmptyView())
}

2 Comments

If possible, one should avoid using AnyView in SwiftUI, and instead use the @ViewBuilder wrapper.
For the above comment, see Demystify SwiftUI: developer.apple.com/videos/play/wwdc2021/10022
3

The simpler approach is to just use .map()

It will show a UserProfile if user has a value, and nothing if user is nil:

var user: User? 
var body: some View {
  user.map { UserProfile(user: $0) }
}

If you want to provide a default view for nil value, you can just use the nil-coalescing operator:

var user: User? 
var body: some View {
  user.map { UserProfile(user: $0) } ?? UserProfile(user: .default) 
}

However this requires the two views to be of the same type, and in practice if they're of the same type you're better off just writing UserProfile(user: $0 ?? .default).

What's interesting is the case where the two views are not of the same type. You could just erase their type by wrapping them in AnyView, but it does get a bit cumbersome and difficult to read at this point:

var user: User? 
var body: some View {
  user.map { AnyView(UserProfile(user: $0)) } ?? AnyView(Text("Not logged in"))
}

So in this case, my preferred approach is to use a custom IfLet struct (same name as Procrastin8's, but a different implementation and syntax) that allows me to write something like this, which I find very clear in intent and easy to type.

var user: User? 
var body: some View {
  IfLet(user) { UserProfile(user: $0) } 
    .else { Text("Not logged in") }
}

The code for my IfLet component is as follows.

struct IfLet<Wrapped, IfContent> : View where IfContent: View {
  let optionalValue: Wrapped?
  let contentBuilder: (Wrapped) -> IfContent

  init(_ optionalValue: Wrapped?, @ViewBuilder contentBuilder: @escaping (Wrapped) -> IfContent) {
    self.optionalValue = optionalValue
    self.contentBuilder = contentBuilder
  }

  var body: some View {
    optionalValue.map { contentBuilder($0) }
  }

  func `else`<ElseContent:View>(@ViewBuilder contentBuilder: @escaping () -> ElseContent) -> some View {
    if optionalValue == nil {
      return AnyView(contentBuilder())
    } else {
      return AnyView(self)
    }
  }
}

Enjoy!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.