As I said before we can do all the work in the stores (SOT) and keeping Channel and Movie plain (only properties). Personally I use Active Record to separating the data access from Source of Truth. Also I have cases that I use the some data object factory method in multiple store. And some different but related projects that use the some web services (data access) but not the Source of Truths (stores). Active Record works great for library / frameworks and integration tests. Also in a team we can split the responsibilities with team members… Data Access, Source of Truth, View, Shared, …
Channels tasks
struct Channel {
// Data
var isFavorite: Bool {
didSet {
Task {
try? await MyTVWS.shared.request(resource: "channels/\(id)",
verb: .patch,
payload: ...)
}
}
}
}
struct Channel {
// Data
// Tasks
func addToFavorites() async throws {
try await MyTVWS.shared.request(resource: "channels/\(id)",
verb: .patch,
payload: ...)
}
func removeFromFavorites() async throws {
try await MyTVWS.shared.request(resource: "channels/\(id)",
verb: .patch,
payload: ...)
}
}
Movies management
Example 1 - Use the NotificationCenter to broadcast a Movie change, very common in IOS platform for model change notifications, include the changed movie object in "sender" or "info", add observer on the MovieStore and update / reload / invalidate the store.
extension Notification.Name {
static let movieDidChange = Notification.Name("movieDidChangeNotification")
}
Example 2 - MovieStore as a Single Source of Truth. Be careful with large data sets (memory). In this example you can see why I use Active Record, I can use Movie.movies factory method in many situations.
class MovieStore: ObservableObject {
@Published var featured: [Movie] = []
@Published var currentlyViewing: [Movie] = []
@Published var favorites: [Movie] = []
@Published var byGenres: [Movie.Genre: Movie] = []
@Published var allMovies: [Movie] = [] // current grid
// load, phase, ...
}
struct MovieList: View {
@StateObject private var store = MovieStore()
// mode, ...
var body: some View {
VStack {
Picker("", selection: $mode) { ... }
.pickerStyle(.segmented)
ScrollView {
LazyVStack {
switch mode {
case .home:
MovieRow(..., movies: store.featured)
MovieRow(..., movies: store.currentlyViewing)
MovieRow(..., movies: store.favorites)
case .genres:
ForEach(...) { movies in
MovieRow(..., movies: movies)
}
}
}
}
.environmentObject(store)
}
}
}
Example 3 - MovieStore as movie sync manager for Core Data (in memory or in disk) / relational database. Requires more work and a local data model.
class MovieStore: ObservableObject { ... } // movies sync and management
struct MovieRow: View {
@FetchRequest(...)
private var movies: FetchedResults<Movie>
// ...
}
There’s an unlimited solutions (and system frameworks) for our problems. We just need to think in our model and how designing the model (data-driven SwiftUI nature) that fits app use cases.
Note: For a single feature / data source apps (e.g. Mail, Notes, Reminders, …) we can use a global / single state ObservableObject. But in many apps we made we have many sections / features / data sources and we need more ObservableObjects / Stores (SOT). Also from my experience ObservableObject to ObservableObject communication / observation is not good and can become confuse, I avoid flow like this: View - ObservableObject - ObservableObject - Data Object