Model layer
class MyWebService {
static let shared = MyWebService()
// URLSession instance / configuration
// Environments (dev, qa, prod, test / mocks)
// Requests and token management
var token: String? = nil
func request<T>(/* path, method, query, payload*/) async throws -> T { ... }
func requestAccess(username: String, password: String) async throws { ... }
func revokeAccess() async throws { ... }
}
// Other names: User, UserStore, CurrentUser, ...
class Account: ObservableObject {
@Published var isLogged: Bool = false
@Published var error: Error? = nil
struct Profile {
let name: String
let avatar: URL?
}
@Published var profile: Profile? = nil
enum SignInError: Error {
case mandatoryEmail
case invalidEmail
case mandatoryPassword
case userNotFound
case userActivationNeeded
}
func signIn(email: String, password: String) async {
// Validation
if email.isEmpty {
error = SignInError.mandatoryEmail
return
} else if email.isEmail {
error = SignInError.invalidEmail
return
}
if password.isEmpty {
error = SignInError.mandatoryPassword
return
}
// Submit
do {
try await MyWebService.shared.requestAccess(username: email,
password: password)
isLogged = true
error = nil
} catch HTTPError.forbidden {
error = SignInError.userActivationNeeded
} catch HTTPError.notFound {
error = SignInError.userNotFound
} catch {
self.error = error
}
}
func signOut() async {
// Ignore any error
try? await MyWebService.shared.revokeAccess()
isLogged = false
}
func loadProfile() async {
do {
profile = try await MyWebService.shared.request(...)
error = nil
} catch {
self.error = error
}
}
}
View layer
@main
struct MyApp: App {
@StateObject var account = Account()
var body: some Scene {
WindowGroup {
Group {
if account.isLogged {
TabView { ... }
.task {
async account.loadProfile()
}
} else {
SignInView()
}
}
.environmentObject(account)
}
}
}
struct SignInView: View {
@EnvironmentObject var account: Account
@State private var email: String = ""
@State private var password: String = ""
// Text fields, button, error exception
var body: some View {
ScrollView { ... }
}
func signIn() {
Task {
await account.signIn(email: email,
password: password)
}
}
}
Post
Replies
Boosts
Views
Activity
Again, you don’t need a middle layer, just your model! Everything becomes easy, composable and flexible. And yes! You can do Unit / Integration tests.
UI = f(State) => View = f(Model)
SignIn
// State (shared in view hierarchy), part of your model
class Account: ObservableObject {
@Published var isLogged: Bool = false
func signIn(email: String, password: String) async throws { ... }
}
// View
struct SignInView: View {
@EnvironmentObject private var account: Account
@State private var email: String
@State private var password: String
// Focus, error state
var body: some View { ... }
// Call from button
func signIn() {
// Validation (can be on model side)
// Change error state if needed
Task {
do {
try await account.signIn(email: email,
password: password)
} catch {
// Change error state
}
}
}
}
SignUp
// Data (Active Record), part of model, can be an ObservableObject if needed
struct Registration: Codable {
var name: String = ""
var email: String = ""
var password: String = ""
var carBrand: String? = nil
func signUp() async throws { ... }
}
// State (Store), part of model
class CarBrandStore: ObservableObject {
@Published var brands: [String] = []
// Phase (loading, loaded, empty, error)
func load() async { ... }
}
// View
struct SignUpView: View {
@State private var registration = Registration()
@StateObject private var carBrandStore = CarBrandStore()
// Focus, error state
var body: some View { ... }
// Call from button
func signUp() {
// Validation (can be on model side)
// Change error state if needed
Task {
do {
try await registration.signUp()
} catch {
// Change error state
}
}
}
}
How many years we have to wait to config (without using UIKit) NavigationBar / TabBar standard, scrollEdge and compact appearance?
How many years we have to wait to:
Change TextField placeholder color?
Custom TextFieldStyle (and include isFocused: Bool)?
How many years we have to wait to change TextEditor background?
The model is composed by Data Objects (structs), Service Objects (providers, shared classes) and State Objects (observable objects, “life cycle” / “data projection” classes). We should use the state objects for specific (or related) data and functionality, not for screen / view, as they should be independent from specific UI structure / needs, When needed we can share then in view hierarchy.
Remember (using state objects in views):
StateObject - strong reference, single source of truth
EnvironmentObject / ObservedObject - weak reference
Also (when async calls needed):
Define state objects (or base class) as MainActor to avoid warnings and concurrency problems
Define state object tasks as async, e.g “func load() async”, because for some situations you need to do other jobs after data load completed and async is more simple / sequencial than checking the “phase” (.loaded) using onChange(of:)
Big apps can have more models. This is just an example:
WWDC 2022 - Data Essentials in SwiftUI
I've seen a lot of questions about MVVM (in general and about testability) unnecessary limitations & problems in WWDC22 SwiftUI digital lounge.
I think developers should start understand & think SwiftUI, instead another platform's strategy.
Apple patterns has been used very successfully by Cocoa developers for decades. It just works, and you don't have to fight it. Jumping into more complicated patterns is not a good approach for developers. Your self, your team, your company and your users will thank you!
Remember:
Avoid sacrificing software design for testability
Automated tests didn’t ensure a quality product
Last years I see a correlation between the increased reliance on automated testing and the decline in quality of software.
A real project that failed and I fixed, one of many problematic projects!
Companies must be careful with devs obsessed with SOLID principles and testability.
In 20 years of software engineering I've never seen soo bad & inefficient developers than in last years. In case of iOS there are very bad developers that come from Web or Android. They fight the system with other platforms concepts and testability talk.
This is software engineering! KISS (Keep It Simple)
This is SOLID (complicated and obsessed people call this clean…. are you kidding?!)
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.
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
}
State and Data Flow
Manage Model Data in Your App
WWDC09-204
WWDC09-226
WWDC20-10040
WWDC21-10022
WWDC22-10054
MVVM vs SwiftUI
Mail app
App Store app
Shopping app
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.