skip to Main Content

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?

3

Answers


  1. 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?

    Login or Signup to reply.
  2. $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))
    
    Login or Signup to reply.
  3. 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
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search