Post

Replies

Boosts

Views

Activity

Reply to Stop using MVVM for SwiftUI
Hi, first if you are making a framework / library (stateless in many cases) your FileItem is perfect. Active Record Pattern struct FileItem { var name: String var size: Int64 static var all: [FileItem] = { ... } static var favourites: [FileItem] = { ... } func setAsFavourite(isFavourite: Bool) { ... } } Repository Pattern struct FileItem { var name: String var size: Int64 } class FileItemRepository { func getAll() -> [FileItem] { ... } func getFavourites() -> [FileItem] { ... } func setFile(_ file: FileItem, asFavourite: Bool) { ... } } If you are making an App (stateful) you need a state. Think about single source of truth(s) and use ObservableObject for external state, you can have one or many state objects. All depends on your app needs but Keep It Simple. You can keep your FileItem with tasks or not, depends. Example #1 (assuming favorite is a file attribute or a reference) struct FileItem { var name: String var size: Int64 func setAsFavourite(isFavourite: Bool) { ... } } class FileStore: ObservableObject { @Published var all: [FileItem] = [] var favourites: [FileItem] { … } // filter favourites from all var isLoading: Bool = false // if needed var error: Error? = nil // if needed func load() async { … } // load all files, manage states (loading, error) if needed } struct MyApp: App { @StateObject var store = FileStore() var body: some Scene { WindowGroup { NavigationView { MyListView(.all) MyListView(.favourites) } .environmentObject(store) .task { await store.load() } } } } Example #2.1 (assuming favorite is another file) struct FileItem { var name: String var size: Int64 } class FileStore: ObservableObject { @Published var all: [FileItem] = [] @Published var favourites: [FileItem] = [] var isLoading: Bool = false // if needed var error: Error? = nil // if needed func load() async { … } // load all files and favourites files, manage states (loading, error) if needed func setFile(_ file: FileItem, asFavourite: Bool) { ... } } struct MyApp: App { @StateObject var store = FileStore() var body: some Scene { WindowGroup { NavigationView { MyListView(.all) MyListView(.favourites) } .environmentObject(store) .task { await store.load() } } } } Example #2.2 (assuming favorite is another file) struct FileItem { var name: String var size: Int64 } class FileStore: ObservableObject { @Published var files: [FileItem] = [] enum Source { case all case favourites } var source: Source var isLoading: Bool = false // if needed var error: Error? = nil // if needed func load() async { … } // load all files or favourites files, manage states (loading, error) if needed func setFile(_ file: FileItem, asFavourite: Bool) { ... } } struct MyApp: App { var body: some Scene { WindowGroup { NavigationView { MyListView(FileStore(.all)) MyListView(FileStore(.favourites)) } } } } …or… struct MyApp: App { @StateObject var allFileStore = FileStore(.all) @StateObject var favouriteFileStore = FileStore(.favourites) var body: some Scene { WindowGroup { NavigationView { MyListView() .environmentObject(allFileStore) MyListView() .environmentObject(favouriteFileStore) } } } } Example #2.3 (assuming favorite is another file) struct FileItem { var name: String var size: Int64 } class FileStore: ObservableObject { @Published var files: [FileItem] = [] var isLoading: Bool = false // if needed var error: Error? = nil // if needed open func load() async { … } // to subclass func setFile(_ file: FileItem, asFavourite: Bool) { ... } } class AllFileStore: FileStore { open func load() async { … } // load all files, manage states (loading, error) if needed } class FavouritesFileStore: FileStore { open func load() async { … } // load favourites files, manage states (loading, error) if needed } struct MyApp: App { var body: some Scene { WindowGroup { NavigationView { MyListView(AllFileStore()) MyListView(FavouriteFileStore()) } } } } …or… struct MyApp: App { @StateObject var allFileStore = AllFileStore() @StateObject var favouriteFileStore = FavouriteFileStore() var body: some Scene { WindowGroup { NavigationView { MyListView() .environmentObject(allFileStore) MyListView() .environmentObject(favouriteFileStore) } } } } Tips: Don’t think ViewModel, think state (view independent) that is part of your model You can have one or many ObservableObjects (external state) Use ObservableObjects when needed and don’t forget view local state EnvironmentObject is your best friend! Keep It Simple In my 3 professional / client SwiftUI apps I made, I learned that EnvironmentObject is critical for many situations. Also, many things become problematic (or impossible) if I think about ViewModels.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
For an Application you can use active record for that. This works great for a lib, data access, data model. struct File: Identifiable, Equatable, Hashable {     var id: UUID = UUID()     var name: String     var date: Date     var size: Int64 var isFavourite: Bool // change attribute or reference on set / didSet func save() func delete() static var allFiles: [File] { … } static var onlyFavouriteFiles: [File] { … } } struct Folder: Identifiable {     var id: UUID = UUID()     var name: String     var files: [File] // or computed property that fetch (on demand) files from this folder static var folders: [Folder] { … } } But in SwiftUI (declarative view layer) you can also need a state(s). You can have a FileStore, FolderStore, FileManagement, … that is part of your model. Assuming that we use system FileManager and you load the items sync when needed. class FileManagement: ObservableObject { var folders: [Folder] = Folder.folders func createFile(…) { … } // do changes, call objectWillChange.send() func deleteFile(…) { … } // do changes, call objectWillChange.send() func setFileAsFavourite(…) { … } // do changes, call objectWillChange.send() } struct MyApp: App { @StateObject var fileManagement = FileManagement() @State var selectedFolder: Folder? = nil @State var selectedFile: File? = nil var body: some Scene { WindowGroup { // Three-column NavigationSplitView { FolderList($selectedFolder) } content: { if let folder = selectedFolder { FileList($selectedFile, files: folder.files) // or FileList($selectedFile, folder: folder) } else { Text(“Select a folder”) } } detail: { if let file = selectedFile { FileView(file: file) // details, attributes, preview, … } else { Text(“Select a file”) } } .environmentObject(fileManagement) } } } Note: For changes use FileManagment methods, not File / Folder methods (FileManagement will call them). Also you can just do the operations in FileManagement and remove File / Folder methods.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
// Do the operations in disk class FileManagement: ObservableObject { var folders: [Folder] = Folder.folders func createFile(…) { … } // do changes, call objectWillChange.send() func deleteFile(…) { … } // do changes, call objectWillChange.send() func setFileAsFavourite(…) { … } // do changes, call objectWillChange.send() } // Do the operations in memory class FileManagement: ObservableObject { // or class FolderStore: ObservableObject { @Published var folders: [Folder] = [] func loadFolders() { folders = Folder.folders } // or func load() { folders = Folder.folders } func createFile(…) { … } // change the folders property hierarchy func deleteFile(…) { … } // change the folders property hierarchy func setFileAsFavourite(…) { … } // change the folders property hierarchy } Also you can just use only the File and Folder active record with a FileWatcher (notify file system changes): struct File: Identifiable, Equatable, Hashable {     var id: URL { url } var url: URL     var name: String     var date: Date     var size: Int64 var isFavourite: Bool // change attribute or reference on set / didSet func save() func delete() static var allFiles: [File] { … } static var onlyFavouriteFiles: [File] { … } } struct Folder: Identifiable {     var id: URL { url } var url: URL     var name: String     var files: [File] // or computed property that fetch (on demand) files from this folder static var folders: [Folder] { … } } struct MyApp: App { @StateObject var fileWatcher = FileWatcher() // Notify file system changes @State var selectedFolder: Folder? = nil @State var selectedFile: File? = nil var body: some Scene { WindowGroup { // Three-column NavigationSplitView { FolderList($selectedFolder, folders: Folder.folders) } content: { if let folder = selectedFolder { FileList($selectedFile, files: folder.files) // or FileList($selectedFile, folder: folder) } else { Text(“Select a folder”) } } detail: { if let file = selectedFile { FileView(file: file) // details, attributes, preview, … } else { Text(“Select a file”) } } } } }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Also, you can use FileManagement as the only SSOT. Note: File and Folder structs can be simple structs, not an active records, you do all the things in FileManagement. class FileManagement: ObservableObject { var folders: [Folder] { Folder.folders } @Published var selectedFolder: Folder? = nil var files: [File] { selectedFolder?.files ?? [] } @Published var selectedFile: File? = nil func startMonitoringChanges() // call objectWillChange.send() on changes func stopMonitoringChanges() // Folder and file operations here if needed / wanted } struct MyApp: App { @StateObject var fileManagement = FileManagement() var body: some Scene { WindowGroup { // Three-column NavigationSplitView { FolderList() } content: { FileList() } detail: { FileView() // details, attributes, preview, … } .environmentObject(fileManagement) .onAppear(perform: fileManagement.startMonitoringChanges) } } } Remember: Don’t worry about “reloading” folders and files, SwiftUI will check the differences and only update what changes. This is the reason why we should use Identifiable protocol. Demistify SwiftUI - WWDC 2021 Session
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
1 - Active Record (like Data Mapper or Repository) is a pattern, very popular, for data access, we use where needed, I use some example to show that model is not only property’s structs. For a library you can use Product, Order as active record but for an app you should include a ProductStore (state), OrderStore (state)… also you can implement all Product, Order tasks on Store and not use Product, Order as active record at all. Remember: typically a framework, like StoreKit 2, need to be flexible, more stateless. A framework is used by an app. For an app like SwiftUI you need one or more state objects, stateful. 2 - Yes, objectWillChange.send() do the @Published job, also @Published is a convenience for objectWillChange.send() and use it. See 2019 post about this, initially (SwiftUI betas)we don’t have @Published. Forget in last posts, you should call objectWillChange.send() before you change the property, not after, be careful with asyncs stuffs. @Published var selectedFolder: Folder? = nil = var selectedFolder: Folder? = nil { willSet { objectWillChange.send() } } 3 - Yes! 4 - As I said in 1 point, you can handle everything on ObservableObject and keep only property structs, and just add some needed tasks to structs. struct Product {     // Properties (data)     // Tasks     func purchase() async throws { ... }     // Factory Method     static products: [Self] {         get async throws {            return try await WebService.shared.request(…)         }     } } class ProductStore: ObservableObject {     @Published var products: [Product] = []          func load() async {          do {             products = try await Product.products         } catch {             // handle error         }     } } or struct Product {     // Properties (data)     // Tasks     func purchase() async throws { ... } } class ProductStore: ObservableObject {     @Published var products: [Product] = []          func load() async {          do {             products = try await WebService.shared.request(…)         } catch {             // handle error         }     } } or struct Product {     // Properties (data) } class ProductStore: ObservableObject {     @Published var products: [Product] = []          func purchase(_ product: Product) async throws { ... }     func load() async {          do {             products = try await WebService.shared.request(…)         } catch {             // handle error         }     } } 5 - In active record you use static func / vars for “Factory Method”, not single instance. For this pattern you only use shared (single instance) for your service / provider objects (e.g. WebService.shared.request(…)). And in general for SwiftUI you should avoid singleinstance for non service / provider objects, use @EnvironmentObject.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
ObservableObject is a working object where we aggregate related data and tasks. We see ObservableObject as: Model object Business object State, life-cycle, management object User case or related use cases object ObservableObject (working, in-memory data) from: Computed Disk (local) Database (local) Network (remote) System Services Example: My last app (multiplatform) have in-app purchases (use StoreKit 2), I have a SubscriptionStore (ObservableObject) for: Load my web service features (multiplatform) Load StoreKit products from features (call Product.products(featureIDs)) refreshPurchasedProducts (handle Transaction.currentEntitlements) check feature availability based on StoreKit purchase / or my web service information (multiplatform) I can add purchase task to SubscriptionStore but use product.purchase() from StoreKit 2 in View. As I use this object in different views I use @EnvironmentObject to have one instance and access from any view in hierarchy. The app use 2 data access models based on Active Record, the my data (web service) model (part active record, part handle some things in the stores) and the StoreKit 2 model.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Remember: Active Record is about data access, not state. SwiftUI views need a state, local (@State) and external (@StateObject). Imagine the CoreLocation 2 using await / async, no delegates. Now we use CLLocation (property only struct) and CLLocationManager (object). In future we could use Location as Active Record: try await Location.current (gives current user location) for await location in Location.updates (gives every locations changes, async sequence) Also, how the long wait SwiftData (Core Data next generation) be like: We have Active Record for data access in PHP Laravel database and Swift server-side Vapor (Fluent database).
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Easy, the MVVM comes from old technology. Also Microsoft, who sell MVVM from 2005 is not using it on new UI framework (declarative). SwiftUI is new tech, modern, declarative. They eliminate the middle layer. Is part of evolution, simplicity. We only need a View layer and a Model layer. Inside Model layer we can do what we want (also some VMs I see on blogs and tutorials are state / store, part of the model). We can’t use MVVM for SwiftUI without problems and limitations. Talking and wanting MVVM or MVC today is the same wanting C++ or Pascal. No one want go back! There’s a modern and more easy techs. The evolution: C -> C++ -> Java / C# -> Swift MVC -> MVVM -> MV SwiftUI automatically performs most of the work traditionally done by view controllers. Fact: SwiftUI View is a ViewModel. To remember! The model layer are not (and never was) only property structs. The model layer is our data, services / networking, state, business objects, processors, … Many devs don’t understand the MVC / MVVM. Fact: VM (reactive) = C (imperative)
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Another generic problem that we should solve - caching (because we fetch some data from a server and we store it on the device, while the app is running, or even persist it between different sessions.) Also this data may be updated from time to time based on other users (imagine a chat functionality in an app). Who should be responsible for storing that data? It's not the view. It's not the controller. Guess, who? - The model. Yes 🙌 Finished an app with caching (local data sync from remote), our model have few “stores” as source of truth to do that job, and again EnvironmentObject is our best friend.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Another Example We can have a single store for everything (not recommended for big, multi section / tab apps) or multi stores, separated by use case / data type / section / tab. Again, stores (ObservableObjects), we can call other names, are part of the model (yes we can separate from our data model). Also we can do everything in stores and not use active record pattern at all but my last experience tell me that is good to separate our data model (using a data access pattern) from state (we can call store object, business object, use case object, state object, external source of truth object, …). This is the SwiftUI (declarative UI) approach. With MVVM pattern: Requires middle layer and more code Need one ViewModel for each View Problems with shared state (e.g. EnvironmentObject) Problems with local state (e.g. FocusState, GestureState, …) Overall platform conflicts Overall external data management limitations Duplicating… SwiftUI View is a “ViewModel” Massive ViewModels (yes can happen)
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Who needs a ViewModel today? Little, simple and clean code. Everything works great on iOS, iPadOS, macOS, tvOS, Unit / Integration Tests server. Very productive team with well defined responsabilities. Note: Just an example based on real SwiftUI app.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Channels There’s many ways for it, depending on our needs. Just some ideas. Typically a small & limited data set For cache use HTTP / URLSession caching system (defined by the server) For offline use the stores (no needed in many cases) Model layer - Example 1 struct Channel: Identifiable, Hashable, Codable { let id: String // Data var isFavorite: Bool // Factory Methods static var channels: [Self] { get async throws { try await MyTVWS.shared.request(resource: "channels", verb: .get) } } } ​ class ChannelStore: ObservableObject { @Published var channels: [Channel] = [] var favorites: [Channel] { channels.filter { $0.isFavorite } } @Published var isLoading: Bool = false var loadError: Error? = nil func load() async { isLoading = true do { channels = try await Channel.channels } catch { loadError = error } isLoading = false } } Model layer - Example 2 Check if channel is favorite on the “favorites” store. struct Channel: Identifiable, Hashable, Codable { let id: String // Data func addToFavorites() async throws { ... } func removeFromFavorites() async throws { ... } // Factory Methods static var channels: [Self] { get async throws { try await MyTVWS.shared.request(resource: "channels", verb: .get) } } static var favoriteChannels: [Self] { get async throws { try await MyTVWS.shared.request(resource: "favoriteChannels", verb: .get) } } } ​ // 2.1 class ChannelStore: ObservableObject { @Published var channels: [Channel] = [] @Published var favoriteChannels: [Channel] = [] @Published var isLoading: Bool = false var loadError: Error? = nil func load() async { isLoading = true do { channels = try await Channel.channels favoriteChannels = try await Channel.favoriteChannels } catch { loadError = error } isLoading = false } } ​ // 2.2 class ChannelStore: ObservableObject { @Published var channels: [Channel] = [] @Published var isLoading: Bool = false var loadError: Error? = nil enum Source { case all case favorites } private let source: Source init(_ source: Source) { self.source = source } func load() async { isLoading = true do { switch source { case .all: channels = try await Channel.channels case.favorites: channels = try await Channel.favoriteChannels } } catch { loadError = error } isLoading = false } } ​ // 2.3 class ChannelStore: ObservableObject { @Published var channels: [Channel] = [] @Published var isLoading: Bool = false var loadError: Error? = nil open func load() async { } } ​ class AllChannelStore: ChannelStore { func load() async { isLoading = true do { channels = try await Channel.channels } catch { loadError = error } isLoading = false } } ​ class FavoriteChannelStore: ChannelStore { func load() async { isLoading = true do { channels = try await Channel.favoriteChannels } catch { loadError = error } isLoading = false } } ​ // 2.4 class ChannelStore: ObservableObject { @Published var channels: [Channel] = [] @Published var isLoading: Bool = false var loadError: Error? = nil open func loadChannels() async throws { } func load() async { isLoading = true do { try await loadChannels() } catch { loadError = error } isLoading = false } } ​ class AllChannelStore: ChannelStore { func loadChannels() async throws { channels = try await Channel.channels } } ​ class FavoriteChannelStore: ChannelStore { func loadChannels() async throws { channels = try await Channel.favoriteChannels } } View layer - Based on Example 1 struct ChannelList: View { @EnvironmentObject private var channelStore: ChannelStore enum Mode { case all case favorites } @State private var mode: Mode = .all var body: some View { VStack { Picker("", selection: $mode) { ... } .pickerStyle(.segmented) ScrollView { LazyVStack { switch mode { case .all: ForEach(channelStore.channels) { channel in ChannelCard(channel: channel) } case .favorites: ForEach(channelStore.favoriteChannels) { channel in ChannelCard(channel: channel) } } } } } } } ​ struct ChannelCard: View { var channel: Channel var body: some View { ... } } ​ struct ProgramList: View { @EnvironmentObject private var channelStore: ChannelStore var body: some View { ... } } ​ struct LivePlayerView: View { @EnvironmentObject private var channelStore: ChannelStore var body: some View { ... } }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Model vs Form (View) What if the data format in your model does not correspond with how you want to show it on screen? Depends on your needs. From my experience I find using local state (1.2) for form data, then convert to your model the best approach. Form should follow the Model. Remember View = f(Model). struct RegistrationInfo: Codable { var name: String = "" var email: String = "" var phone: String = "" var age: Int = 19 func submit() async throws { ... } } ​ // Convenience if needed extension RegistrationInfo { var firstName: String { String(name.split(separator: " ").first ?? "") } var lastName: String { String(name.split(separator: " ").last ?? "") } } ​ // 1.1 - Using view local state (direct), need a tricky solution struct RegistrationForm: View { @State private var info = RegistrationInfo() @State private var isSubmitting: Bool = false @State private var submitError: Error? = nil var body: some View { Form { Section("Name") { TextField("First name", text: Binding(get: { info.firstName }, set: { value, _ in ???? })) TextField("Last name", text: Binding(get: { info.lastName }, set: { value, _ in ???? })) } // ... Button("Submit") { Task { isSubmitting = true do { try await info.submit() } catch { submitError = error } isSubmitting = false } } } } } ​ // 1.2 - Using view local state (indirect) struct RegistrationForm: View { @State private var firstName: String = "" @State private var lastName: String = "" @State private var email: String = "" @State private var phone: String = "" @State private var age: Int = 18 @State private var isSubmitting: Bool = false @State private var submitError: Error? = nil var body: some View { Form { Section("Name") { TextField("First name", text: $firstName) TextField("Last name", text: $lastName) } // ... Button("Submit") { Task { isSubmitting = true do { let info = RegistrationInfo(name: "\(firstName) \(lastName)", email: email, phone: phone, age: age) try await data.submit() } catch { submitError = error } isSubmitting = false } } } } } ​ // 2 - Using an external state, object part of your model class Registration: ObservableObject { @Published var firstName: String = "" @Published var lastName: String = "" @Published var email: String = "" @Published var phone: String = "" @Published var age: Int = 18 @Published var isSubmitting: Bool = false var submitError: Error? = nil func finish() async { isSubmitting = true do { let data = RegistrationInfo(name: "\(firstName) \(lastName)", email: email, phone: phone, age: age) try await data.submit() } catch { submitError = error } isSubmitting = false } } ​ struct RegistrationForm: View { @State private var registration = Registration() var body: some View { Form { Section("Name") { TextField("First name", text: $registration.firstName) TextField("Last name", text: $registration.lastName) } // ... Button("Submit") { Task { await registration.finish() } } } } }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
About the service object Active Record pattern StoreKit 2 approach // Handles requests, environments, tokens, … // General access to an specific web service using an URLSession instance class MyTVWS { static let shared = MyTVWS() func request(…) async throws -> T { … } } struct Channel: Codable { // Data (properties) // Factory Methods static var channels: [Self] { get async throws { try await MyTVWS.shared.request(resource: "channels", verb: .get) } } } struct Movie: Codable { // Data (properties) // Factory Methods static func movies(pageNumber: Int = 1, pageSize: Int = 30) async throws -> [Self] { try await MyTVWS.shared.request(resource: "movies", verb: .get, queryString: ...) } } Advantages: Better code and object organization Direct object task access Works great for modular (multi model) architectures Easy for team member responsibilities Perfect for scalability and maintenance Clean and easy to use True OOP and Separation of Concerns approach Disadvantages: SOLID and anti-OOP principles / patterns devs don’t like it Massive service object strategy, POJOs WeatherKit approach, from a team (Dark Sky) that Apple acquired // Handles requests, environments, tokens, … // Specific access to an specific web service using an URLSession instance class MyTVWS { static let shared = MyTVWS() func getChannels() async throws -> [Channel] { try await MyTVWS.shared.request(resource: "channels", verb: .get) } func getMovies(pageNumber: Int = 1, pageSize: Int = 30) async throws -> [Movie] { try await MyTVWS.shared.request(resource: "movies", verb: .get, queryString: ...) } } struct Channel: Codable { // Data (properties) } struct Movie: Codable { // Data (properties) } Advantages: Simple data objects Disadvantages: Massive single service object (many responsibilities) Code fragmentation (e.g. Channel related code and functionality present in different files / objects) Scalability and maintenance problems (e.g. many devs working / changing on single object with many responsibilities)
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22