2

I am trying to update @Published arrays in separate view models, one view model is a singleton whilst the other is not as there can be multiple instances.

  • The purpose of the code below is to allow a user to save or unsave movie recommendations to their personal movie collection (savedMovies).
  • The MyMovies class is a singleton, this is intentional as there is only one user.
  • The MovieShop class is not a singleton as there can be multiple movie shops and therefore multiple instances.
  • When a user presses "Save" on a movie name, the movie should be added to their saved movies (MyMovies class) and the movie must be shown as "Saved" in the Movie Recommendation list (MovieShop class).
  • When the movie is removed from the users savedMovie array, in all movie shops (only one shown in the example code) the movie that was removed from the savedMovie array must have its saved state reset to false so so it can be saved again (by the user pressing "Save").

The implementation below works when pressing the save/unsave button for each movie in the movie recommendations view but not for the users saved movies view (as it removes the movie entirely).

My question is this:

I have been looking at the Apple Developer Docs in an attempt to understand the correct way to go about doing this when working within the SwiftUI framework. So far I haven't had much luck finding an answer. Is the proposed solution within the Apple Guidelines or is there another way in which i should go about solving this problem?

Any help would be greatly appreciated.

// Movie type
struct Movie: Hashable {
    let name: String
    var saved: Bool
}

@MainActor final class MyMovies: ObservableObject {
    @Published var savedMovies: [Movie] = [Movie(name: "Ice Age", saved: true)]
    
    static let shared = MyMovies()
    
    private init() {}
    // function to remove a movie from the users saved movies
    func unsaveMovie(_ movie: Movie) {
        savedMovies.removeAll(where: {$0.name == movie.name})
        // Need to also toggle saved state of unsaved/removed movie so
        // its state is set to "Save" in movie shop (not sure how)
    }
}

@MainActor class MovieShop: ObservableObject {
    @Published var recommendations: [Movie] = [
        Movie(name: "Ice Age", saved: true),
        Movie(name: "Star Wars", saved: false),
    ]
    // function to save or unsave a movie depending on if it is
    // already in the users saved movies array or not
    func toggleMovieSaved(_ movie: Movie) {
        let index = recommendations.firstIndex(where: {$0.name == movie.name})!
        recommendations[index].saved.toggle()
        
        // Check if user already has the movie saved
        let alreadySaved = MyMovies.shared.savedMovies.contains(where: {$0.name == movie.name})
        
        if alreadySaved {
            // Remove movie from users saved movies
            MyMovies.shared.savedMovies.removeAll(where: {$0.name == movie.name})
        } else {
            // Save/Add movie to users saved movies
            var newMovie = movie
            newMovie.saved = true
            MyMovies.shared.savedMovies.append(newMovie)
        }
    }
}
// I have combined the movie shop view and user saved movies view into one
// for demo purposes
struct ContentView: View {
    @StateObject private var myMovies = MyMovies.shared
    @StateObject private var movieShop = MovieShop()
    
    var body: some View {
        VStack {
            Text("My Saved Movies")
                .font(.title)
            ForEach(myMovies.savedMovies, id: \.name) { movie in
                HStack {
                    Text(movie.name)
                    Button("Remove") {
                        myMovies.unsaveMovie(movie)
                    }
                }
            }
            Divider()
            Text("Movie Recommendations")
                .font(.title)
            ForEach(movieShop.recommendations, id: \.name) { movie in
                HStack {
                    Text(movie.name)
                    Button(movie.saved ? "Unsave" : "Save") {
                        movieShop.toggleMovieSaved(movie)
                    }
                }
            }
        }
    }
}

1 Answers1

1

Usually there is just one object that loads/saves/stores all the different model struct arrays, e.g.

class ModelStore: ObservableObject {
    var savedMovies: [Movie]
    var recommendations: [Movie]

Since Movie is a struct you might have better luck using arrays of IDs instead for the saved and recommendations then lookup an allMovies array - see favouriteSmoothies in the Fruta sample.

You might be able to solve your problem of multiple objects instead by using a View struct with a binding. In SwiftUI think of the View struct as the View model, and the view hierarchy transforms from complex model types, e.g. your Movie into simple value types (for easy previewing).

malhal
  • 26,330
  • 7
  • 115
  • 133
  • Thanks for your help, I looked at the Fruta sample and see what you mean by using IDs instead. However, I don't understand what you mean by thinking of a View struct as the View model. Would the binding take the singleton's `savedMovies` array and then you create a function within the View to check if a movie is already saved? Also if I remove a movie from the `savedMovies`, how do I update each individual movie `recommendations` array to reflect the change, should that done through a function in a View as well? – IamActuallyIronMan Jul 04 '23 at 11:09