[Beginner] SwiftUI: Why isn't this working?

This a very basic question I guess - I would expect that tapping on the button in the "first view" would display the "next view". Instead, it seems to reload "first view" which attempts to force unwrap an optional that is now nil.
(an instance of ModuleManager is created in SceneDelegate and passed to ContentView).

What did I miss in my SwiftUI training??

thank you :pray:

import SwiftUI

class ModuleManager : ObservableObject {
    @Published var string_A : String? = "Hello A"
    @Published var string_B : String? = "Hello B"
}

struct ContentView: View {
    @ObservedObject var moduleManager : ModuleManager
    var body: some View {
        VStack{
            if moduleManager.string_A != nil {
                FirstView(moduleManager:moduleManager)
        }
        else if moduleManager.string_B != nil{
            NextView(moduleManager:moduleManager)
        }
        }
    }
}

struct FirstView: View {
    @ObservedObject var moduleManager : ModuleManager

    var body: some View {
        Button(action: {
                   self.moduleManager.string_A = nil}){
    Text(moduleManager.string_A!)}
    }
}

struct NextView: View {
    @ObservedObject var moduleManager : ModuleManager
    var body: some View {
        Button(action: {
                       self.moduleManager.string_B = nil}){
        Text(moduleManager.string_B!)}
        }
}

SwiftUI does not make any guarantee about the View update orders. In this particular case, it (directly) invalidates both FirstView and ContentView and choose to update FirstView before ContentView (hereby updating FirstView again) resulting in the error.

What I'd suggest that you do the checking in the FirstView instead, since they would know best whether to display the view.

struct FirstView: View {
    @ObservedObject var moduleManager : ModuleManager

    @ViewBuilder
    var body: some View {
        if moduleManager.string_A != nil {
            Button(action: { self.moduleManager.string_A = nil }) {
                Text(moduleManager.string_A!)}
            }
        }
    }
}

Another solution would be to not observe moduleManager in the FirstView, simply because the its parent is already observing it.

struct FirstView {
    let moduleManager: ModuleManager
    
    var body: some View {
        Button(action: { self.moduleManager.string_A = nil }){
            Text(moduleManager.string_A!)
        }
    }
}

Even better, use Binding instead, which I deem more appropriate for this scenario:

struct ContentView: View {
    @ObservedObject var moduleManager : ModuleManager
    var body: some View {
        VStack{
            if moduleManager.string_A != nil {
                ResetView(string: $moduleManager.string_A)
            } else if moduleManager.string_B != nil{
                ResetView(string: $moduleManager.string_B)
            }
        }
    }
}

struct ResetView: View {
    @Binding var string: String?
    
    var body: some View {
        Button(action: { self.string = nil }) {
            Text(string!)
        }
    }
}

This works because it only (directly) invalidate ContentView, which in turn update every one of its children. You still need to make sure to nil-check in the parent view in this case, though.

2 Likes

Thanks a lot. Very informative answer!

Terms of Service

Privacy Policy

Cookie Policy