Post

Replies

Boosts

Views

Activity

Reply to Stop using MVVM for SwiftUI
Checkout process example SwiftUI approach // Model layer class Checkout: ObservableObject { ... } // View layer struct CheckoutView: View {     @StateObject private var checkout = Checkout()          var body: some View {         NavigationStack {             CheckoutProductInfoForm()             // -> CheckoutOffersView()             // -> CheckoutBuyerForm()             // -> CheckoutDeliveryInfoForm()             // -> CheckoutSummaryView()         }         .environmentObject(checkout)     } } Advantages: Clean, simple and data-driven development Core for declarative UI platforms Checkout model object is independent from a specific View and platform Works great for multiplatform (inside Apple ecosystem) Disadvantages: Other platform devs don’t (yet) understand it MVVM approach // Need a model or helper object to share / joint data between the VMs // ViewModel layer class CheckoutProductInfoViewModel: ObservableObject { ... } class CheckoutOffersViewModel: ObservableObject { ... } class CheckoutBuyerViewModel: ObservableObject { ... } class CheckoutDeliveryInfoViewModel: ObservableObject { ... } class CheckoutSummaryViewModel: ObservableObject { ... } // View layer struct CheckoutView: View {     var body: some View {         NavigationStack {             CheckoutProductInfoView() // <- CheckoutProductInfoViewModel             // -> CheckoutOffersView() <- CheckoutOffersViewModel             // -> CheckoutBuyerView() <- CheckoutBuyerViewModel             // -> CheckoutDeliveryInfoView() <- CheckoutDeliveryInfoViewModel             // -> CheckoutSummaryView() <- CheckoutSummaryViewModel         }     } } Advantages: Sometimes we feel that using VMs can help to avoid massive views (but not really necessary -> SwiftUI component nature) Disadvantages: A middle layer, unnecessary, more objects / files, more code, more complexity Not easy to handle some use cases, becomes problematic in some situations Not easy to share a state in view hierarchy ViewModel-View dependence becomes bad for multiplatform (iOS UI != iPad UI != TV UI …) Old approach, not suitable for declarative platforms Can fight the SwiftUI platform
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
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
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Movies Typically a big & unlimited 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 struct Movie: Identifiable, Hashable, Codable { let id: String // Data enum Section: String, Codable { case all case featured case favorites case currentlyViewing // ... } enum Genre: String, Identifiable, Codable, CaseIterable { case action case comedy case drama case terror case animation case science case sports case western // ... var id: Self { self } } // Factory Method static func movies(pageNumber: Int = 1, pageSize: Int = 30, search: String? = nil, section: Movie.Section = .all, genres: [Movie.Genre] = [], sort: String? = nil) async throws -> [Self] { try await MyTVWS.shared.request(resource: "movies", verb: .get, queryString: ...) } // --- or --- // (recommended) struct FetchOptions { var pageNumber: Int = 1 var pageSize: Int = 30 var search: String? = nil var section: Section = .all var genres: [Genre] = [] var sort: String? = nil } static func movies(_ options: FetchOptions) async throws -> [Self] { try await MyTVWS.shared.request(resource: "movies", verb: .get, queryString: ...) } } ​ class MovieStore: ObservableObject { @Published var movies: [Movie] = [] @Published var isLoading: Bool = false var loadError: Error? = nil var options: Movie.FetchOptions init(_ options: Movie.FetchOptions) { self.options = options } // Add convenience initializings if wanted init(_ section: Movie.Section, genre: Movie.Genre? = nil limit: Int = 15) { self.options = ... } func load() async { isLoading = true do { movies = try await Movie.movies(options) } catch { loadError = error } isLoading = false } func loadNextPage() async { ... } // Infinite scrolling } View layer struct MovieList: View { enum Mode { case home case genres } @State private var mode: Mode = .home var body: some View { VStack { Picker("", selection: $mode) { ... } .pickerStyle(.segmented) ScrollView { LazyVStack { switch mode { case .home: MovieRow(store: MovieStore(.featured)) MovieRow(store: MovieStore(.currentlyViewing)) MovieRow(store: MovieStore(.favorites)) case .genres: ForEach(Movie.Genre.allCases) { genre in MovieRow(store: MovieStore(.all, genre: genre)) } } } } } } } ​ // Each row can be limited to n items and have a "View all" button to push MovieGrid with all items struct MovieRow: View { @StateObject var store: MovieStore var body: some View { ... } } ​ struct MovieGrid: View { @StateObject var store: MovieStore var body: some View { ... } } ​ struct MovieCard: View { var movie: Movie var body: some View { ... } }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’22
Reply to Stop using MVVM for SwiftUI
Just watched the session “13 - Lessons learnt rewriting SoundCloud in SwiftUI - Matias Villaverde & Rens Breur” from NSSpain. They rewrite the UIKit app to SwiftUI using “Clean Architecture” + “View Models”… and guess… they failed! Again, learn and understand SwiftUI paradigm before start a project. Don’t fight the system, Keep It Simple!
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Oct ’22
Reply to Stop using MVVM for SwiftUI
Software Engineering (design / modeling / specs) is one of the most important work of our jobs. I’m trying to find a simple diagram for SwiftUI to: Identify Objects (structs / classes) Identify Tasks (methods) Identify Dependencies A modified version of old (but gold) DFD (Data-flow diagram) fits very well on SwiftUI data-driven architecture. Also helps to understand how SwiftUI works. I’m calling it “Data-driven diagram”. The “data element” in diagram is independent of Data Access Patterns (Active Record, Repository, POJOs, …). Shopping Example Books App Example
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Nov ’22
Reply to Stop using MVVM for SwiftUI
Hi, posted something about that in older posts, for big projects you can have specific models (modules). You never end up with a big objects. I do large projects very easy to do and productive for the team. Scale very well and with clear (true) “separation of the concerns“. The goal of architecture is to make your life (or your team’s life) easier. If this is not happening, then your architecture has failed. …and sorry to repeat… Model != Entities Model objects represent special knowledge and expertise. They hold an application’s data and define the logic that manipulates that data. The model is a class diagram containing the logic, data, algorithms, … of the software. For example, you can think Apple Platform as a big project: // Can be internal (folders inside project) or external (sub-modules, packages) if makes sense // Independent and focused teamwork // Easy to build, test and maintenance Contacts (model) ContactsUI EventKit (the model) EventKitUI Message (the model) MessageUI CoreLocation (the model) CoreLocationUI Shared SharedUI [module] [module]UI Or a TV app: User (model) UserUI LiveTV (the model) LiveTVUI VODMovies (the model) VODMoviesUI VODSeries (the model) VODSeriesUI Shared SharedUI Everyday I see many, small to big, Clean / VIP / VIPER projects with a massive Entities (…Interactors, Presenters, …) folders without a clear separation of the things where all the team members work on that (all things), creating problematic situations and complexity hell. Recently we watch the bad experience (NSSpain video) from SoundCloud team and how “Uncle Bod” things broke the promise.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Nov ’22
Reply to SwiftUI slowly adoption
Some things that works on iOS 16: Refreshable modifier work on ScrollView We can change TextEditor background hiding the default background “.scrollContentBackground(.hidden)” Change TextField placeholder font and color “…, prompt: Text(…).font(…).foregroundColor(…)” - as of iOS 15
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Nov ’22
Reply to NavigationStack suggestions
It seems that now (iOS 16) we can apply button styles to NavigationLink. NavigationLink("Show Terms") {     TermsView() } .buttonStyle(.borderedProminent) .controlSize(.large) I don’t know if is iOS 16 only or not but this is huge improvement. Thanks
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Nov ’22
Reply to Stop using MVVM for SwiftUI
Another guy leaving out MVVM. Is testability really more important than using SwiftUl as intended? …sacrifice a lot of great SwiftUl's built in APIs. From my experience we don’t need VMs (MVVM) for testability or scalability.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Feb ’23
Reply to Stop using MVVM for SwiftUI
Everything depends on your needs. The “store” works like the working memory. We load & aggregate the data we need (to use and manipulate) from different sources. It become the source of truth and can be used (and shared) by many “views”. React / Flutter / SwiftUI patterns are modern evolution of old patterns (e.g. MVVM, MVC, …). Data struct Channel: Identifiable, Codable { let id: String let name: String let genre: String let logo: URL? // Factory Methods static var all: [Self] { … } static var favorites: [Self] { … } } Example 1 - Handle individual sources class ChannelStore: ObservableObject { @Published var channels: [Channel] = [] @Published var isLoading: Bool = false var loadError: Error? = nil func loadAll() async { isLoading = true do { channels = try await Channel.all } catch { loadError = error } isLoading = false } func loadFavorites() async { isLoading = true do { channels = try await Channel.favorites } catch { loadError = error } isLoading = false } } Example 2 - Aggregate all related information 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.all favoriteChannels = try await Channel.favorites } catch { loadError = error } isLoading = false } }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’23
Reply to Stop using MVVM for SwiftUI
Hi Eddie, YES, correct and more… ViewModel also represents the presentation logic / state like “showConfirmationAlert”, “filterBy”, “sortBy”, “focus”, … In SwiftUI you can use @State properties for that. In MVVM the View state is binding to the ViewModel object. Store is more in “model” (or data) side. Yes you can use Store in UIKit (MVC) but remember that in Apple MVC the “Store” is what many of us call the “Model Controller”. (e.g. NSFetchedResultsController, NSArrayController, UIDocument, …). In declarative platforms UI = f(State) (data-driven approach) you don’t need a middle man (controller, viewmodel, …). The ”Stores” are part of the model and works like the working memory, we use it to load / aggregate the data we need to handle (process, work, …). I call “Store” (from React and WWDC videos) but we can call what we want and makes sense. We should just avoid to mix it with MVVM pattern.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’23