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.
Post
Replies
Boosts
Views
Activity
I see this message too when go back in NavigationView stack. Everything seems fine, maybe a Xcode / iOS 16 bug since they deprecated NavigationView. I see other people complaining about that using early Xcode betas.
I think we should ignore that message and wait for Xcode 14.1.
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)
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).
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.
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.
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
// 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”)
}
}
}
}
}
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.
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.
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 { … }
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.
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.
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 { ... }
}
}
How some developers, obsessed by testability & SOLID principles, think “separation of concerns”, making something simple, ..complex, problematic, expensive.