skip to Main Content

Swift beginner here. I am trying to create a reusable component that simply renders either a filled or empty systemImage circle based on a value stored in state. I’ve put together a reproduction example below. Right now it only appears to be updating the value in state (in formData), but the UI doesn’t seem to be updating (the circle is not filling up). I have a feeling the Image and Text components are set up to react to state changes, but I’m not sure how to fix from there. Thank you in advance!

import SwiftUI

struct Test: View {
    @ObservedObject var formData = FormData()

    var body: some View {
        VStack {
            Text("Performance: (String(formData.goals.performance))")
            Toggle(isOn: Binding(
                get: { formData.goals.performance },
                set: { formData.goals.performance = $0 }
            )) {
                Text("Performance")
            }
            ChildWrapper(isPerformanceEnabled: $formData.goals.performance)
        }
    }
}

struct ChildWrapper: View {
    @Binding var isPerformanceEnabled: Bool

    var body: some View {
        VStack {
            Image(systemName: isPerformanceEnabled ? "circle.fill" : "circle")
                .font(.system(size: 50))
                .foregroundColor(isPerformanceEnabled ? .green : .red)
                .onTapGesture {
                    isPerformanceEnabled.toggle()
                }
            Text(isPerformanceEnabled ? "Enabled" : "Disabled")
        }
    }
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

2

Answers


  1. If you use this:

    import SwiftUI
    
    struct Test: View {
       // @ObservedObject var formData = FormData()
    @State private var isPerformanceEnabled = false
        var body: some View {
            VStack {
                Text("Performance: (String(isPerformanceEnabled))")
                Toggle(isOn: $isPerformanceEnabled) {
                    Text("Performance")
                }
                ChildWrapper(isPerformanceEnabled: //$formData.goals.performance)
                             $isPerformanceEnabled)
            }
        }
    }
    
    struct ChildWrapper: View {
        @Binding var isPerformanceEnabled: Bool
    
        var body: some View {
            VStack {
                Image(systemName: isPerformanceEnabled ? "circle.fill" : "circle")
                    .font(.system(size: 50))
                    .foregroundColor(isPerformanceEnabled ? .green : .red)
                    .onTapGesture {
                        isPerformanceEnabled.toggle()
                    }
                Text(isPerformanceEnabled ? "Enabled" : "Disabled")
            }
        }
    }
    
    struct Test_Previews: PreviewProvider {
        static var previews: some View {
            Test()
        }
    }
    

    maybe work but is not the complete solution for you. I think you do check the correct binding $ of the ObservedObject.

    Login or Signup to reply.
  2. The problem is in your model (FormData).

    Let’s examine the code:

    @ObservedObject var formData = FormData()

    1. This bit is incorrect. You need @StateObject. Here is why:

    @ObservedObject implies that ownership is not local. For example you could have an object higher up in the hierarchy or a singleton passed to this view.

    The documentation states:

    Add the @ObservedObject attribute to a parameter of a SwiftUI View when the input is an ObservableObject and you want the view to update when the object’s published properties change. You typically do this to pass a StateObject into a subview.

    2. There is no reason to pass a custom Binding to your toggle. Just pass:

    Toggle(isOn: $formData.goals.performance)

    3. A typical implementation of your model could look like this:

    class FormData: ObservableObject {
        
        struct Goals {
            var performance: Bool = false
        }
        
        @Published var goals = Goals()
    }
    

    note here that Goals is a struct, which means that whenever any of its properties change, goals would be re-assigned and FormData would emit an objectWillChange() triggering your view to re-evaluate the body.


    Bonus Bit:
    Here is an interesting fact that many people get wrong…

    ChildWrapper got a binding in order to be able to change isPerformanceEnabled, which is correct.
    But what is exactly causing the re-evaluation?
    The answer is, not the change to the binding, but the change to FormData (through the binding) that re-evaluated the parent view!
    In fact, if the binding changed something that not an ancestor view would be watching, no re-evaluation would happen.

    That’s all. I hope that this makes sense.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search