Post

Replies

Boosts

Views

Activity

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
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
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.
Nov ’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
Nov ’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!
Oct ’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
Sep ’22
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
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)
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 { ... } }
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 { ... } }
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() } } } } }
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.
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)
Sep ’22