chornbe
chornbe

Reputation: 1163

SwiftUI view unexpectedly dismissing on change of state object values

Weird behavior where changing state is auto-dismissing a view.

Xcode 13.2.1, MacOS 12.2.1, iPhone 13 simulator iOS 15.something.

I'm genning up an object in the root view and passing it into the environment using @environmentObject(...), navigating via a NavigationView in the root view, updating things along the way (in this case the object is a data collector for account signup information across several screens). I get a few screens in, and whammo, any udpates to the...

@EnvironmentObject private var signupInfo: SignupInfo 

...object causes a dismiss. It's not consistent (meaning which screen will do it), but whatever screen it happens on, happens 100% of the time.

I don't think there's anything overly fancy about my data collector object, and even on a stripped-to-the-bone test View, it still happens (pasted below).

I can make it not happen by not using a ride-along object in the environment and collecting into local state, or into a view model, but I still have to collect it all in on place at some point. Without using a global object, or persisting it somewhere, I don't really like any other complex ideas. This should just work. ** scratches head **

The data collector object

import Foundation

enum AccountType: Int {
    case none
    case journalOnly
    case singleIncident
    case preventive
}

class SignupInfo : ObservableObject {
    // seeding sample data for less typing during dev & testing
    @Published var email: String = "[email protected]"
    @Published var password: String = "skatanna99!"
    @Published var password2: String = "skatanna99!"
    @Published var firstname: String = "bob"
    @Published var lastname: String = "bobberson"
    @Published var address1: String = "123 Some Town Road"
    @Published var address2: String = ""
    @Published var city: String = "Newark"
    @Published var region: String = "DE"
    @Published var postalCode: String = "19701"
    @Published var phone: String = ""
    @Published var ccnumber: String = ""
    @Published var ccname: String = ""
    @Published var accountType: AccountType = .journalOnly
}

(not real info of course)

The view that's crashing out or dismissing on data change. I wondered if it was a threading thing, so I have with and without a dispatch. No change to the behavior. I think it's related to the depth into the navigationLink stack.

import SwiftUI

struct SignupServicesView: View {
    
    //@StateObject private var viewModel:ViewModel = ViewModel()
    @EnvironmentObject private var signupInfo: SignupInfo
    //@EnvironmentObject private var opData: OpData
    
    var body: some View {
        Text("account type is set to \(signupInfo.accountType.rawValue)")
            .onTapGesture {
                signupInfo.accountType = .preventive
            }
            .onLongPressGesture {
                DispatchQueue.main.async {
                    signupInfo.accountType = .preventive
                }
            }
    }
    
}

The root view is literally as simple (in this stripped down test) as like: (example)

root view -> Creates @State object (signupInfo = SignupInfo()) -> NavigationView -> Link to next View with .environmentObject(signupInfo)

I also tried creating the state and sending it into the environment earlier in a previous view before the NavigationView even came into existence. Didn't change anything.

Thoughts...?

^^^^^^ edit / update ^^^^^^

Interestingly, when I move things into the viewModel, it all works normal, as expected. No surprise. But I still want a common place to drop the data, without creating CoreData or UserDefaults entries.

When I move the underlying data collector object into a global context and attach it to the view model, it works if I hop the properties through the view model (view updates view model, view model updates global object).

But if I just expose the underlying data connector directly it still dismisses the View in this crazy weird way.

Hang the global object off the app's main view (for testing only) public static var signupInfo = SignupInfo()

Then in the viewModel... import Foundation

extension SignupServicesView {
    class ViewModel : ObservableObject {
        // works great
        @Published var firstname = theApp.signupInfo.firstname
    }
}

And the View: import SwiftUI

struct SignupServicesView: View {
    
    @StateObject private var viewModel:ViewModel = ViewModel()
    
    var body: some View {
        Text("account type is set to \(viewModel.firstname)")
            .onTapGesture {
                viewModel.firstname = "joe"
            }
            .onLongPressGesture {
                DispatchQueue.main.async {
                    viewModel.firstname = "joe"
                }
            }
            .navigationBarHidden(true)
    }
}

That all works fantastic.

Exposing the data directly doesn't work. The viewModel: import Foundation

extension SignupServicesView {
    class ViewModel : ObservableObject {
        @Published var signupInfo = theApp.signupInfo
    }
}

The View: import SwiftUI

struct SignupServicesView: View {
    
    @StateObject private var viewModel:ViewModel = ViewModel()
    
    var body: some View {
        Text("account type is set to \(viewModel.signupInfo.firstname)")
            .onTapGesture {
                viewModel.signupInfo.firstname = "joe"
            }
            .onLongPressGesture {
                DispatchQueue.main.async {
                    viewModel.signupInfo.firstname = "joe"
                }
            }
            .navigationBarHidden(true)
    }
}

So, it really feels like talking to my SetupInfo object directly is a problem for (some of) my Views no matter how they become aware of it, but damned if I can figure out why.

Upvotes: 3

Views: 1724

Answers (1)

ChrisR
ChrisR

Reputation: 12165

The classic way would be to have the data model as a struct and publish that via class view model. Not sure if this works in your case (where do you get the data from?) but it would look like this:

enum AccountType: Int {
    case none
    case journalOnly
    case singleIncident
    case preventive
}

// Data model as struct, not class
struct SignupInfo {
     var email: String = "[email protected]"
     var password: String = "skatanna99!"
     var password2: String = "skatanna99!"
     var firstname: String = "bob"
     var lastname: String = "bobberson"
     var address1: String = "123 Some Town Road"
     var address2: String = ""
     var city: String = "Newark"
     var region: String = "DE"
     var postalCode: String = "19701"
     var phone: String = ""
     var ccnumber: String = ""
     var ccname: String = ""
     var accountType: AccountType = .journalOnly
}


// global view model to publish the data model
class ViewModel : ObservableObject {
    @Published var signupInfo = SignupInfo()
}


// views, this one is creating the viewmodel, after that you can pass it down by environmentObject
struct SignupServicesView: View {
    
    @StateObject private var viewModel : ViewModel = ViewModel()
    
    var body: some View {
        Text("account type is set to \(viewModel.signupInfo.firstname)")
            .onTapGesture {
                viewModel.signupInfo.firstname = "joe"
            }
            .onLongPressGesture {
                viewModel.signupInfo.firstname = "Tom"
            }
            .navigationBarHidden(true)
    }
}

Upvotes: 1

Related Questions