Home > Mobile >  unwrapping optional @State in SwiftUI when applied to a Binding
unwrapping optional @State in SwiftUI when applied to a Binding

Time:08-26

I'm looking for a clean solution to resolve this SwiftUI challenge.

The following code compiles but do not work since @State property is outside the ContentView scope.

import SwiftUI

struct ContentView: View {
  var state: LocalState?
  
  var body: some View {
    if let state = state {
      Toggle("Toggle", isOn: state.$isOn)
    }
  }
}

extension ContentView {
  struct LocalState {
    @State var isOn: Bool
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    VStack {
      ContentView(
        state: .init(isOn: false)
      )
      .border(Color.red)
      
      ContentView()
        .border(Color.red)
    }
    
  }
}

The following code doesn't compile since the following reasons:

Value of optional type 'ContentView.LocalState?' must be unwrapped to refer to member 'isOn' of wrapped base type 'ContentView.LocalState'

It seems that $ in $state.isOn refer to the original state and not to the unwrapped one.

import SwiftUI

struct ContentView: View {
  @State var state: LocalState!
  
  var body: some View {
    if let state = state {
      Toggle("Toggle", isOn: $state.isOn)
    }
  }
}

extension ContentView {
  struct LocalState {
    var isOn: Bool
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    VStack {
      ContentView(
        state: .init(isOn: false)
      )
      .border(Color.red)
      
      ContentView()
        .border(Color.red)
    }
  }
}

What I do NOT want is:

  • use of failable initializer in ContentView.
  • move isOn property outside LocalState.

How can I achieve those?

CodePudding user response:

This works for me:

var body: some View {
    if let isOn = Binding($state)?.isOn {
        Toggle("Toggle", isOn: isOn)
    }
}

Breaking it down: $state is a Binding<LocalState?>, and we use the Binding initialiser (hopefully that's not the failable initialiser that you don't want to use) to convert it to a Binding<LocalState>?. Then we can use optional chaining and if let to get a Binding<Bool> out of it.

Related: How can I unwrap an optional value inside a binding in Swift?

CodePudding user response:

I believe this can be solved with two techniques. 1. using the Binding constructor that can create a non-optional binding from an optional. And 2. use of a constant binding in previews, e.g.

import SwiftUI

struct Config {
    var isOn: Bool
}

struct ContentView: View {
    @State var config: Config?
    
    var body: some View {
        if let config = Binding($config) { // technique 1
            ContentView2(config: config)
        }
    }
}

struct ContentView2: View {
    @Binding var config: Config
    
    var body: some View {
        Toggle("Toggle", isOn: $config.isOn)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView2(config: .constant(Config(isOn: false))) // technique 2
    }
}

CodePudding user response:

$state is syntactic sugar for _state.projectedValue, which gives you a Binding<LocalState?>. And from here on things are ugly.

You might be able to get away with a wrapped binding:

var wrappedIsOn: Binding<Bool> {
    let stateBinding = $state
    return Binding {
        stateBinding.wrappedValue?.isOn ?? false
    } set: {
        stateBinding.wrappedValue?.isOn = $0
    }
}

And then:

Toggle("Toggle", isOn: wrappedIsOn)

And alternative, inspired by @Sweeper's answer:

Toggle("Toggle", isOn: Binding($state)?.isOn ?? Binding.constant(false))
  • Related