Post

Replies

Boosts

Views

Activity

Reply to Stop using MVVM for SwiftUI
Simple software design can only be achieved by really fully understanding the problem First you have to really understand what’s going on, in all its complexities, and then come up with a solution so simple that – in hindsight – it is the obvious way to do it. Don't over-engineer your code Don't fall into the tutorial trap Don't just copy and paste code without understanding it Don't blindly follow someone else's strategy Avoid sacrificing software design for testability Consider mocking only as a last resort Be careful, in an effort to make code more testable, we can very often find ourselves introducing a ton of new protocols and other kinds of abstractions, and end up making our code significantly more complicated. There's an object for that! Identify Objects Identify Tasks Identify Dependencies Don’t fight the system Don’t literally start with a protocol Avoid unnecessary internal & external dependencies Avoid too many layers of abstraction or complexity Use nested type for supporting or to hide complexity
Jan ’22
Reply to Stop using MVVM for SwiftUI
We are living in Object Oriented Programming (OOP) world! Don't do Clean / SOLID anti-patterns, be an OOP engineer In the agile world use Feature Driven Development (FDD) Avoid as you can Test Driven Development (TDD) / Extreme Programming (EP) Yes, Swift is POP (the OOP where you can use structs as objects and protocols for generalization).
Jan ’22
Reply to Stop using MVVM for SwiftUI
In app, you store and process data by using a data model that is separate from its UI Adopt the ObservableObject protocol for model classes Use ObservableObject where you need to manage the life cycle of your data Typically, the ObservableObject is part of your model
May ’22
Reply to Stop using MVVM for SwiftUI
StoreKit 2 is a great example how to create your model using Active Record and Factory Method patterns. StoreKit 2 Remember: SwiftUI automatically performs most of the work traditionally done by view controllers (view models too). Declarative UI does not require MVVM. We are now in an era where declarative UI is commonplace in iOS development. The other things you can do with property wrappers, modifiers and custom views.
May ’22
Reply to Stop using MVVM for SwiftUI
Everyday I see bad developers, normally coming from other platforms, ignoring the obvious. In the field of React/Vue/Flutter, which also uses declarative UI, the architecture of MVVM is not adopted, and it seems strange to adopt MVVM only in SwiftUI.
May ’22
Reply to Stop using MVVM for SwiftUI
Part I Be careful, in an effort to make code more testable, we can very often find ourselves introducing a ton of new protocols and other kinds of abstractions, and end up making our code significantly more complicated. Avoid sacrificing software design for testability and consider mocking only as a last resort. Model (Data / Networking / Algorithms) objects represent special knowledge and expertise. They hold an application’s data and define the logic that manipulates that data. The model layer is not simple structs (POJOs). A model is a system of objects (structs / classes) with properties, methods and relationships. For simple apps we can define one model, for complex apps we can have many models as needed (and makes sense). Popular Model design patterns: Active Record, Data Mapper and Factory Method are the key' to avoid massive ViewControllers / ViewModels (and unnecessary layers). See “The Features of the Main Data Access Patterns Applied in Software Industry” by Marcelo Rodrigues de Jesus Model is everything your app (or part / system) can do, in a pure OOP. View is just an interaction layer and handle presentation logic. See WWDC2010 Session - MVC Essential Design Pattern for Flexible Software See StoreKit 2 documentation for an example of the model of In-App Purchases. See MusicKit documentation for an example of the model of Music Catalog. Normally Apple split frameworks, but not exclusive, into (e.g.): Contacts (non-UI, model layer) ContactsUI (UI, view layer) Remember not all your code need tests, people are over-testing today. In many cases if you design the software applying the best patterns you will never need a test at all. Tests, pull-requests, ... are talked today because people don't plan & design software, people create many dependencies & layers making isolations / unchanged code difficult. Think your software, just focus on your model(s) and your objects! About the tests: Unit Tests (should used where there's non other objects / systems integration, test your model) Integration Tests (should used where there's other objects / systems integration, test your model) UI Tests (test your UI and presentation logic) But an ugly guy said: "Ahhh you should do Unit Tests for integrations integrations, use mocks for them" and boom! This guy killed the software design. Wrong!! First you should avoid this and second there's many ways to mock without complicate or sacrifice the software design. Default rule: Use Unit Tests only when you need to test a calculation, algorithm, ... not when you do a request to web service. Don’t fall in unit test troll! See “Unit Testing is Overrated” by Oleksii Holub “Unit Tests” project in Xcode is Unit Tests and Integration Tests.
May ’22
Reply to Stop using MVVM for SwiftUI
Part II This is the reason for Massive View Controllers. When you define the model as POJOs (simple structs) you end with many logic inside the ViewControllers / ViewModels. VC and VM should only handle presentation logic. In many cases impossible to test non-ui. Also theres a bad dependencies on you non-ui code. To avoid this you end with unnecessary layers. And… you have to make change for multiplatform views. But if you go OOP & software design patterns, everything will be perfect. Also, as you see, every object become independent because “Service Objects” just gives the access / door, don’t know what the other objects want. Whenever possible, each object should be concrete and independent. If we add / remove / move a object, the others should keeping working and remaining unchanged. Your model is ready for every platform views, terminal, tests, … NOTE: Imagine the CLLocationManager using await / async, no delegates. And yes you can do an locations manager if need. Hope new frameworks this WWDC from ground for Swift and Swift Concurrency. CLLocation and CLLocationManager just become Location: try await Location.current (gives current user location) for await location in Location.updates (gives every locations changes, async sequence)
May ’22
Reply to Stop using MVVM for SwiftUI
Part III Shopping App The Model Layer KISS - Simple design become more easy to do, more easy to maintain / scale, more easy to find & fix bugs, more easy to test. You can use in UIKit, SwiftUI, iOS, tvOS, macOS, Terminal, Unit Tests, …hey! You can make a Swift Package and share with other apps. It works like magic! In a big app you can have more models “Workout model” + Nutrition model”, “LiveTV model” + “VideoOnDemand model” + “Network & Subscription model”, … You can use the “TEST” (like DEBUG) flag for mocks, in WebService provider level but also in your factory methods. Also you can extend the provider and override the request method… there are 1001 ways to mock… but remember don’t sacrifice the simplicity for testing. The View Layer A view has a body and handle presentation logic. SwiftUI automatically performs most of the work traditionally done by view controllers (and view models), but what about the other work like fetch states or pagination? SwiftUI is very good at composable / components and there are modifiers and property wrappers, just use your imagination and the power of the SwiftUI… yes you don’t need extra layers, in last resort make utility objects (e.g. FetchableObject) in your “Shared” folder. With Swift Concurrency everything becomes easy to use and to reuse. @AsyncState private var products: [Product] = [] List { … } .asyncState(…) Example of fetch state property wrapper + modifier. import SwiftUI public enum AsyncStatePhase { case initial case loading case empty case success(Date) case failure(Error) public var isLoading: Bool { if case .loading = self { return true } return false } public var lastUpdated: Date? { if case let .success(d) = self { return d } return nil } public var error: Error? { if case let .failure(e) = self { return e } return nil } } extension View { @ViewBuilder public func asyncState<InitialContent: View, LoadingContent: View, EmptyContent: View, FailureContent: View>(_ phase: AsyncStatePhase, initialContent: InitialContent, loadingContent: LoadingContent, emptyContent: EmptyContent, failureContent: FailureContent) -> some View { switch phase { case .initial: initialContent case .loading: loadingContent case .empty: emptyContent case .success: self case .failure: failureContent } } } @propertyWrapper public struct AsyncState<Value: Codable>: DynamicProperty { @State public var phase: AsyncStatePhase = .initial @State private var value: Value public var wrappedValue: Value { get { value } nonmutating set { value = newValue } } public var isEmpty: Bool { if (value as AnyObject) is NSNull { return true } else if let val = value as? Array<Any>, val.isEmpty { return true } else { return false } } public init(wrappedValue value: Value) { self._value = State(initialValue: value) } @State private var retryTask: (() async throws -> Value)? = nil public func fetch(expiration: TimeInterval = 120, task: @escaping () async throws -> Value) async { self.retryTask = nil if !(phase.lastUpdated?.hasExpired(in: expiration) ?? true) { return } Task { do { phase = .loading value = try await task() if isEmpty { self.retryTask = task phase = .empty } else { phase = .success(Date()) } } catch _ as CancellationError { // Keep current state (loading) } catch { self.retryTask = task phase = .failure(error) } } } public func retry() async { guard let task = retryTask else { return } await fetch(task: task) } public func hasExpired(in interval: TimeInterval) -> Bool { phase.lastUpdated?.hasExpired(in: interval) ?? true } public func invalidate() { if case .success = phase { phase = .success(.distantPast) } } } extension View { @ViewBuilder public func asyncState<T: Codable, InitialContent: View, LoadingContent: View, EmptyContent: View, FailureContent: View>(_ state: AsyncState<T>, initialContent: InitialContent, loadingContent: LoadingContent, emptyContent: EmptyContent, failureContent: FailureContent) -> some View { asyncState(state.phase, initialContent: initialContent, loadingContent: loadingContent, emptyContent: emptyContent, failureContent: failureContent) } } Hope popular needs / solutions implemented in SwiftUI out of the box.
May ’22
Reply to Stop using MVVM for SwiftUI
I understand your concerns but in case of SwiftUI (declarative) the “MVVM” (original from 2005) is not the correct approach. Maybe the problem is people calling ViewModel and MVVM. Let me explain, in MVVM almost every View most have a ViewModel, basically you have a middle layer (data binding). In SwiftUI (declarative) you don’t need that, in SwiftUI you can use the concept of “Store” to do the odd jobs. In fact this is a “model type” but we should avoid call ViewModel (and MVVM) because the conflict. This is MVVM: HomeView - HomeViewModel ProductListView - ProductListViewModel ProductDetailView - ProductDetailViewModel ProfileView - ProfileViewModel This is SwiftUI: CurrentProfile (or in generic Current<Profile>) ProductStore (or in generic Store<Product>) AppState / NavigationState HomeView ProductListView ProductDetailView ProfileView Focus on the model (data + business logic) Create views that reflect the model If needed use a model type (e.g. Store) in your view layer to do odd jobs But why I’m complain about the use of MVVM for SwiftUI? Because I worked for Microsoft platforms (community leader) and evangelist the MVVM from 2005.
Jun ’22
Reply to Stop using MVVM for SwiftUI
SwiftUI is a modern UI framework and in the declarative world MVVM (original) feels unnecessary and very limited. The problem is people understand what MVVM architecture is in fact. Maybe they are just calling every non-view objects (e.g. store, state, current, …) ViewModels but this is not MVVM.
Jun ’22
Reply to NavigationStack suggestions
Another suggestion. In many cases we don’t use the NavigationLink because we have a custom UI and can’t change de pressed style. Will be great to have a “NavigationLinkStyle” (with isPressed) or a NavigationButton or data-drive push capabilities for the Button. In fact Button is used for push all the time.
Jun ’22
Reply to NavigationStack suggestions
Another suggestion. Example from docs: NavigationStack { List(parks) { park in NavigationLink(park.name, value: park) } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } } In this case, we are duplicating the “park” data because that park is already in the stack. The navigation stack contains the park and we are making an unnecessary copy for ParkDetails view. Why not to use the @Environment to get the pushed value (in this case the park) on ParkDetail? Every pushed View most have the corresponding value available in the @Environment. Before: struct ParkDetail: View { let park: Park } After: struct ParkDetail: View { @Environment(\.pushedData) var park: Park } Also we can do the same for other cases like modals: struct ParkPreview: View { @Environment(\.presentedData) var park: Park }
Jun ’22
Reply to Stop using MVVM for SwiftUI
Sorry, the examples are not testable? Not clean? Today developers are obsessed with tests and other ugly thinks. I see projects delayed with lots problems because devs today want to be SOLID and testable. Agile, SOLID and over testing works like a cancer for development teams. Sorry but if people want to be complicated they should move to Android or other platforms.
Jun ’22