Stop using MVVM for SwiftUI

Don’t over-engineering! No suggested architecture for SwiftUI, just MVC without the C.

On SwiftUI you get extra (or wrong) work and complexity for no benefits. Don’t fight the system.

If the user wants to favorite channels and VOD movies, how would you handle that?

1- A list of [Channel] and [Movie] in your AccountStore (user state/data) ? 2- Or if we want to keep only the ID (eg. [Channel.ID] + [Movie.ID]), we need to map these ids to their object after app launch. Is it the Account that access the data? or do you have a nested dependency to access the data only via ChannelStore and VODStore (requiring these stores load the data before to not have empty collection)

ChannelStore.load
VideoOnDemand.load
Account.load + map Channel/VOD.ID.

3- Can different store access the same underlying data via your Active record object? or if a View needs data from 2 different store to map them to a new object type.

4- Also curious about an active record example. what is inside these "..." in the static methods or functions. Movie.all { ??? } do you always call Webservice.shared.allMovies? are you using a cache? return Cacheservice.shared.allMovie ?? Webservice.shared.allmovies

thank you!

Simple question: what if the data format in your model does not correspond with how you want to show it on screen? E.g. it's not simply a String in your model that you show in a TextField component, but you need to split it over 3 different TextField components and merge it when you put it back in the model?

Unless I am mistaken I haven't seen one word about converting data.

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()
                }
            }
        }
    }
}

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 { ... }
}

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 { ... }
}

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)

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

Thank you for your examples.

case .home:
    MovieRow(store: MovieStore(.featured))
    MovieRow(store: MovieStore(.currentlyViewing))

Here, if a Movie is editable (for the example), Movie.A is edited in the first MovieRow/Grid -> Editor. And, this same movie is in the second row (currentlyViewing), it won't be refreshed. A Store with a computed var to filter movies can fix this problem here (or even a binding of a movie subset on the global list). it's more an app problem and how to manage the underlying data (local chache, persistence etc)^^'

In your

struct Channel: Identifiable, Hashable, Codable {

func addToFavorites() async throws { ... }

}

I guess you have the same type of code with the service object?


struct Channel: Identifiable, Hashable, Codable {

func addToFavorites() async throws {
	self.isFavorite = true
	// or
	try await MyTVWS.shared.favorite(movieId: self.id)
}

}

And if a Store needs Data from 2 types to do his work, you'll do something like this ?

class SearchStore {
	@Published var movies: [Movie] = []
	@Publisehd var channels: [Channel] = []

	func search() async {
        isSearching = true
        do {
            async let movieResults = Movie.search(option: MovieSearchOptions)
            async let channelResults = Channel.search(option: ChannelSearchOptions)
        	let results = try await [movieResults, channelResults]
        	// or could be different to display results as soon as possible for a type
        } catch {
            searchingError = error
        }
        isSearching = false
    }
}

ActiveRecord looks nice but how would change a service implementation (not necessary the environment at init time) as it is tightly coupled in the model. Did you have the problem and a recommended approach? Or in many cases the question to change wasn't even here (to not over engineering before it is needed)?

MyTVWebService.shared.favorite(movieId: self.id) with

MyTVLocalService.shared.favorite(movieId: self.id) to

class MovieStore: ObservableObject {
	// online 
	init(with service: MyTVWS = .remote) {}
	// -- or --
	// local
	init(with service: MyTVWS = .local) {}

}

Anyway, I'll keep reading your posts, some interesting thought and approach :) I was - and still - never a fan of VIPER, TCA, MVVM for everything even if not needed, and ActiveRecord is another approach that I like (+ reading Apple MusicKit / StoreKit2 confirms it - Too bad we can not see what's behind their static method.)

It takes time to think differently than traditionnal OOP. Same than you, I worked on multi layer projects and your "Massive service object strategy" is an exact copy of what we had !

  • other horrible things including useless unit/integration tests.

6-years ago I started a Unity project with ECS structure instead of OOP and it was really fast to do anything. You want to change/ add a SDK => add a new system An analytics event ? => create a new data and any systems can listen and react to this event.

Think SwiftUI with

  • all the app data in a global context
  • ObservableObjects + View are just systems / puzzle pieces on top of that and react only when their own necessary data updates (or is added/removed)

I would love that and that's why I like SwiftUI since day one as it is close to this .

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

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.

Yes with static data, Active Record can be nice. With large dataset, we needs to be careful on the "Single Source of Truth" (memory, data sync, .)

Also from my experience ObservableObject to ObservableObject communication / observation is not good

Agree, I also avoid nested observables. Often it can be resolved with smaller views+logic / container view.

Apple use ECS for GameplayKit. It’s great for Games but don’t see it in Apps today (

Yes we used it for a game (not GameKit). And on a second project it was for everything (Gameplay + UI + Services). When you say View = f(State), it is the same thing with ECS. We have something close with the existing SwiftUI but maybe more later with an official framework or else. With the sheet modifier it is what we had a lot for UI: set a flag, the view controller listen and show the popup.

Global context, Gameplay context, UI context, services context, you can split as needed by the app.

  • Global -> static data, configuration etc
  • Gameplay -> player position, events, game state
  • UI -> UI state, rewards available, current tab, intent to show with priority
  • services -> initialize and handle the logic for services, analytics, socials, persistence, sync etc.

Of course, the ECS performance gain is overkill for a UI based app but the data logic and system execution (≠ OOP) is extremely easy to use, maintainable, and evolvable. Subject for another thread!

Thank you for your detailed posts / examples.

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!

Is it ok to pass environmentObjects to a stateObject using .onAppear if we want that state object to help us manage the data to be read/written in the environmentObject? Just so we don't clutter the View struct with lots and lots of code? For instance when creating code to edit a user profile, where a lot of validation is needed?

e.g.

struct EditPersonalInfoView: View {

  @StateObject var userDataValidation = UserDataValidation()   
  @EnvironmentObject var userInfo: UserInfo

  var body: some View {
    List {
      .... /* a bunch of fields here, validation code inside `userDataValidation` */
    }
    .onAppear {
      userDataValidation.initialize(userInfo: userInfo)
    }
  }
}

We can't pass the userInfo object to userDataValidation in the constructor, for obvious reasons.

The SwiftUI team actually advise against what you're suggesting. Here is a SwiftUI engineer from Apple's most recent QA session.

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

Stop using MVVM for SwiftUI
 
 
Q