Post

Replies

Boosts

Views

Activity

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.
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)
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).
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.
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.
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
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”) } } } } }
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.
Sep ’22
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.
Sep ’22
Reply to NavigationStack suggestions
Another thing that could be change is the need for Hashable, why not use the Identifiable like sheet / fullscreenCover?! SwiftUI is turning model object more and more dependent on the view needs. Before SwiftUI: struct Product: Codable { … } After SwiftUI: (we need for ForEach and sheet / fullscreenCover, ok makes sense, any object should have an id) struct Product: Codable, Identifiable { … } After SwiftUI 4.0: (we need for NavigationStack push item) struct Product: Codable, Identifiable, Hashable { … } Bonus: (if we need to use the onChange) struct Product: Codable, Identifiable, Hashable, Equatable { … }
Aug ’22
Reply to Stop using MVVM for SwiftUI
Sorry but my personal opinion is only about developers must stop follow SOLID and “Uncle Bob” bad practices. The fact is, in world, SOLID projects (small, big) become complex, expensive and unmanageable, this is a fact, there are results / metrics that prove it. You can see the difference on Apple, after Scott Forstall left everything becomes problematic, buggy and delayed. Scott Forstall was not an Agile / CI / CD / SOLID guy and everything platform (and iOS software) release was perfect! Just works! Every developer and iPhone user see the difference. There’s an iOS before Scott and another after. This is a fact! Also, many don’t know but Agile / “Uncle Bod” approach just killed Microsoft. This is very well known during Windows Longhorn (Vista) development, everything becomes problematic, delayed and we never see some great features. The problems stills next years and they lose the smartphone battle. This is a fact! Note: I think (and hope!) that Apple is not a fully (or obsessed) Agile / CI / CD / SOLID company, but new developer generation could bringing it on.
Aug ’22
Reply to Stop using MVVM for SwiftUI
KISS (Keep It Simple) vs SOLID in numbers (real world project work hours from my team) Team KISS Development: 120 hours Change #1: 16 hours Change #2: 2 hours Team SOLID Development: +1500 hours Change #1: 48 hours Change #2: 14 hours Everything becomes problematic, complex, unmanageable and expensive. (no other team members understand and want to fix / change) These results are REAL and the company / managers are upset about the SOLID decision. This is very common in many teams and companies. KISS approach is more productive and easy to test, fix, change.
Aug ’22
Reply to Stop using MVVM for SwiftUI
Model layer class MyWebService {     static let shared = MyWebService          // URLSession instance / configuration     // JSONDecoder/Encoder configuration     // Environments (dev, qa, prod, test / mocks)     // Requests and token management          var token: String? = nil          func request<T: Codable>(/* path, method, query, payload*/) async throws -> T { ... }          func requestAccess(username: String, password: String) async throws { ... }     func revokeAccess() async throws { ... } } struct Product: Identifiable, Hashable, Codable {     let id: Int     let name: String     let description: String     let image: URL?     let price: Double     let inStock: Bool } class ProductStore: ObservableObject {     @Published var products: [Product] = []          enum Phase {         case waiting         case success         case failure(Error)     }          @Published var phase: Phase? = nil          func load() async {         do {             phase = .waiting             products = try await MyWebService.shared.request(path: “products”)             phase = .success         } catch {             phase = .failure(error)         }     } } View layer struct ProductList: View {     @StateObject var store = ProductStore()     var body: some View {         List { ... }             .task {                 await store.load()             }             .navigationDestination(for: Product.self) { product in                 ProductView(product: product)                 // (--- OPTIONAL ---)                 // Only if you need to make changes to the store and that store its not already shared                 // ProductView(product: product)                 //    .environmentObject(store)                 // (--- OR ---)                 // ProductView(product: product, store: store)             }     } } struct ProductView: View {     var product: Product     // (--- OR ---)     // If you make changes to the product     // @State var product: Product         // (--- OPTIONAL ---)     // Only if you need to make changes to the store and that store its not already shared     // @EnvironmentObject private var store: ProductStore     // (--- OR ---)     // @ObservedObject var store: ProductStore          var body: some View {         ScrollView { ... }     } }
Aug ’22