3

Continuing from my previous question, what is the best way to observe several state changes from a UIViewRepresentable? Previously, I required to only observe changes to one property userWon. However, as new functionality was introduced in the app, I need to now observe new changes. Assuming these could increase even more, is the best way to do this by adding more State properties and binding them to the UIViewRepresentable? It feels somewhat hacky at this point to add more and more bindings and onChange modifiers to observe for changes. Example code is as follows:

struct GameView: View {
    @State private var userWon: Bool = false
    @State private var propA: Bool = false
    @State private var propB: Int = 0
    @State private var propC: Int = 0
    ...
    ...
    ...
    @State private var propZ: Int = 0
    @StateObject private var gameService = GameService()
    
    var body: some View {
        ZStack {
            VStack {
                GameViewRepresentable(userWon: $userWon,
                                      propA: $propA,
                                      propB: $propB,
                                      propC: $propC,
                                      ...
                                      propZ: $propZ) // <- Add more bindings??
                    .onChange(of: userWon) { newValue in
                        if newValue {
                           gameService.updateUserScore()
                        }
                    }
                    .onChange(of: propA) { newValue in
                           gameService.doSomethingA()
                    }
                    .onChange(of: propB) { newValue in
                           gameService.doSomethingB()
                    }
                    .onChange(of: propZ) { newValue in
                           gameService.doSomethingZ()
                    }
            }
            .clipped()
        }
    }
}

struct GameViewRepresentable: UIViewRepresentable {
    typealias UIViewType = GameView
    @Binding var userWon: Bool
    
    func makeUIView(context: Context) -> GameView {
        let gameView = GameView()
        gameView.delegate = context.coordinator
        return gameView
    }

    func updateUIView(_ uiView: GameView, context: Context) {
    }

    func makeCoordinator() -> GameViewCoordinator {
        GameViewCoordinator(self)
    }

    class GameViewCoordinator: GameViewDelegate  {
        var parent: GameViewRepresentable
        
        init(_ parent: GameViewRepresentable) {
            self.parent = parent
        }

        func userWon() {
            self.parent.userWon = true
        }
    }
}

Alternate approach that I can think of is:

  1. Create an envelope object/model in gameService that contains the aforementioned properties to be observed and pass that as binding to the UIViewRepresentable.
  2. Have gameService observe this enveloped object, probably using Combine? for changes but I'm not sure how to do this or if it'd work at all. Any help is appreciated on how to do this efficiently.
7
  • Your first alternate approach looks fine to me. What's the problem with it? Commented Sep 25, 2023 at 3:03
  • You don't actually need to make an "envelope object". Just move all these properties into GameViewModel as @Published vars. Commented Sep 25, 2023 at 3:05
  • It didn't feel right in the sense that although I'd be creating the wrapper object in the view model and pass it as a binding to the view representable, I'd still be observing changes using onChange in the view. Commented Sep 25, 2023 at 3:06
  • What I'm trying to achieve is to also de-clutter the implementation by having the properties and its observing mechanism in the same object to say so. Is it possible to combine approaches 1 & 2? i.e. have the properties as Published vars in the view model and observe them using combine? I've never done this before so I don't know if updating the bindings of the properties in the view representable would cause observable changes for the view model to take action on. Commented Sep 25, 2023 at 3:09
  • Yes it is possible to combine 1 and 2, but I feel like the root of the problem is the giant black box that is GameViewRepresentable. If you could split it up into multiple SwiftUI views that has their own state, you wouldn't need so many @State in the parent. The views can then interact with the game model through an @EnvironmentObject or similar. Commented Sep 25, 2023 at 3:17

1 Answer 1

1

You can move all the @State vars into your GameService as @Published vars.

class GameService: ObservableObject {
    @Published var someGameState = 1
    @Published var anotherGameState = 1

    func doSomething() {
        print(someGameState)
    }
    func doSomethingElse() {
        print(anotherGameState)
    }
}

Then you can pass this to GameViewRepresentable through an @ObservedObject, to avoid writing out all the states the game view needs.

struct ContentView: View {
    @StateObject var game = GameService()
    
    var body: some View {
        GameViewRepresentable(game: game)
    }
}

struct GameViewRepresentable: UIViewRepresentable {
    @ObservedObject var game: GameService
    
    // ...
}

Since this is a UIViewRepresentable, I think you would likely be updating game through its properties' setters (rather than say, a Binding produced from the projected value of ObservedObject). In that case, you can just use didSet to detect the changes:

@Published var someGameState = 1 {
    didSet { self.doSomething() }
}
@Published var anotherGameState = 1 {
    didSet { self.doSomethingElse() }
}

Otherwise, changes can be detected using onReceive:

// add .dropFirst() if you don't want the initial value to trigger this
.onReceive(game.$someGameState) { _ in
    game.doSomething()
}
.onReceive(game.$anotherGameState) { _ in
    game.doSomethingElse()
}
Sign up to request clarification or add additional context in comments.

10 Comments

Thanks! I went with moving the properties to the view model and using their setters to keep extra code from being added to the view.
This is SwiftUI so the View struct plus state already is a view model. @StateObject is designed for something else.
@malhal I know that. This seems to be a common misunderstanding of SwiftUI. But this question isn’t about that. We know very little about what GameViewModel does. For all I know, it could just be OP’s data (e.g. the current game state), but OP incorrectly calls it a “view model”, because everyone else is doing it. I’m not going to assume that it is what it says.
@malhal If you just don't like the word ViewModel in my code, I've removed it.
@malhal could you elaborate more? I came across this extract which presumably was taken from an ask Apple event on a question about MVVM: In short, we do not recommend using the view as the viewModel. However, SwiftUI does provide tools to implement the classic MVVM architecture (such as StateObjects, ObservedObjects). It seems to me like its a problem of nomenclature than substance tbh. Like in my case my GameViewModel actually is a game service of sorts and doesn't do any view data preparation.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.