0
votes

I have two views - a MasterView and DetailView. When opening the DetailView, I initialise a new class that tracks data about the view (in the real implementation, the detail view involves a game).

However, when I press the back button from the DetailView to return to the MasterView, and then press the button to return to the DetailView, my class is unchanged. However, I would like to re-initialise a new copy of this class (in my case to re-start the game) whenever I move from the MasterView to the DetailView.

I have condensed the problem to this code:

import SwiftUI
import Combine

class Model: ObservableObject {
    @Published var mytext: String = "mytext"
}

struct MasterView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: DetailView(model: Model())) {
                Text("press me")
            }
        }
    }
}

struct DetailView: View {
    @ObservedObject var model: Model = Model()

    var body: some View {
        TextField("Enter here", text: $model.mytext)
    }
}


struct MasterView_Previews: PreviewProvider {
    static var previews: some View {
        MasterView()
    }
}

I would like to create a new instance of Model every time I click the NavigationLink to the detail view, but it seems like it always refers back to the same original instance - I can see this by typing a change into the text field of the DetailView, which persists if I go back and forward again.

Is there any way of doing this?

1
Without really trying to duplicate this, I think your issue is with initialization - namely, DetailView(model: Model() in what you are passing to DetailView. If you are using SwiftUI, why are you not creating an EnvironmentObject? (You don't need to, but it makes it much easier.) It's the Model() - particularly the () piece - that looks to me like it screwing you. That's the "create a "new instance" or "refer to the same original instance" you are referring to. Creating one instance of your model and use that throughout. - user7014451
Sorry for the second comment - I can post better code to do this if you need me to. - user7014451
I hadn't thought about passing data through the environment, but it's not clear to me that it's needed here. I only really need the Model() object in one view (the DetailView), so the environment wouldn't help. Also, with the environment (the way I understand it) I would only have one instance, but I specifically would like to have multiple instances. The reason I'm passing the model in the MasterView is because in my code I actually have several navigation links - each with a specific model instance, e.g. DetailView(Model(settings=a)) and DetailView(Model(settings=b)) - mxt533
I don't understand - why exactly do you need multiple instances of your model? This sounds.... wrong. If Model == AppState, what am I missing? Stated another way (and using UIKit instead of SwiftUI, but it really doesn't matter), let's say you have three view controllers - A, B, and C. All all embedded in a UINavigationController. VC "A" has a button saying "press me" and when done, VC "B" displays a text field, which is passed to be used by VC "C". Is this pretty much what you want? If so, why do you need more than one instance of everything? I'm sure I'm missing something. - user7014451
Sorry if unclear - it's the first time I'm building such an app, so it may well not be the optimal architecture, but I appreciate your help! I am building a game, and MasterView is the main menu from which you can tap on a difficulty level (each presented by DetailView). Model stores a single game's variables, including difficulty level (this is what I meant by settings=a etc), high score, and gameIsOver. That's why, back in the main menu, clicking on a game again, I want it to restart, and I thought I would achieve this by passing a new instance of Model. Does this make more sense? - mxt533

1 Answers

1
votes

Based on your comments - and correct me where wrong - here's how I'd set things up.

Your needs are:

  • A "base" class. Call it MasterView, "settings", "view state", whatever. This is where everything starts.
  • A "current game".... well, it could be a struct, a class, even properties in an ObservableObject.

I think that's about it. Hierarchically, your model could be:

ViewState

...Player

......Properties, including ID and history

...Current Game

...... Properties, including difficulty

Please note, I've changed some names and am being very vague on properties. The point is, you can encapsulate all of this in an ObservableObject, create an `EnvironmentObject of it, and have all your SwiftUI views "react" to changes in it.

Leaving out views, hopefully you can see where this "model" can contain just about all the Swift code you wish to do everything - now all you need is to tie in your views.

(1) Create your ObservableObject. It needs to (a) be a class object and (b) conform to the ObservableObject protocol. Here's a simple one:

class ViewState : ObservableObject {
    var objectWillChange = PassthroughSubject<Void, Never>()
    @Published var playerID = ""  {
        willSet {
            objectWillChange.send()
        }
    }
}

You can create more structs/classes and instantiate them as needed in your model.

(2) Instantiate ViewState once min your environment. In SceneDelegate:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: ContentView()
            .environmentObject(ViewState())
        )
        self.window = window
        window.makeKeyAndVisible()
    }
}

Note that there's a single line added here and that ViewState is instantiated a single time.

(3) Finally, in any SwiftUI view that needs to know your view state, bind it by adding one line of code:

@EnvironmentObject var model: ViewState

If you want, you can do virtually anything in your model (ViewState) from instantiating a new game, flag something to result in a modal popup, add a player to an array, whatever.

The main thing I hope I'm explaining is there's no need to instantiate a second view state - rather instantiate a second game instance inside your single view state.

Again, if I'm way off from your needs, let me know - I'll gladly delete my answer. Good luck!