I promise, this is not a rant. I’m really trying to wrap my head around the pros and cons of MVVM. As I was building my application I was trying to follow MVVM as best as I could but there were instances where a view did not need need a dedicated view model since it required data that was in another view already.
That’s when I discovered Apples new Observable Macro and how it works under the hood. I tried to use it in a MVVM architecture way but didn’t see what the point was after watching some developer videos:
https://developer.apple.com/videos/play/wwdc2020/10040/
https://developer.apple.com/videos/play/wwdc2023/10149/
In these examples they highlight placing the Observable class into the Environment and using it in child views as needed. Since Observable is very intelligent under the hood it does not cause unnecessary rerenders.
So that’s what I did. I created “Controllers” for a specific part of my application and passed them into the environment at the appropriate parent level.
I don’t even know what architecture I’m following, if any. But it works. It works exactly how I expect an application should.
I created an AuthController that I passed into the environment at the root level. This gave my landing screen access to the log in function and my settings screen access to the log out.
final class AuthController {
func signInWithApple(using idToken: String) async -> () {
do {
let credentials = OpenIDConnectCredentials(provider: .apple, idToken: idToken)
try await supabaseClient.auth.signInWithIdToken(credentials: credentials)
} catch {
print(error)
}
}
func signOut() async -> () {
do {
try await supabaseClient.auth.signOut()
} catch {
print("error", error)
}
}
}
Another example of a controller I created was the UserController. To handle updating the users data (profile) and updating the UI anywhere it may be used.
@Observable final class UserController {
private(set) var userProfile: UserProfile?
func getUserProfile() async -> () {
do {
let user = try await supabaseClient.auth.session.user
let userProfile: UserProfile = try await supabaseClient.database
.rpc("get_user_profile", params: [ "user_id" : user.id.uuidString ])
.select()
.execute()
.value
self.userProfile = userProfile
} catch {
print("error", error)
}
}
func updateUsername(newUsername: String) -> () {
userProfile?.updateUsername(newUsername: newUsername)
}
}
I injected this into the environment at the TabView level so all the application screens have access to the user data and can update them when needed. A quick test of updating the username from two different tab views works as expected.
struct HomeView: View {
@Environment(UserController.self) var userController
var body: some View {
ScrollView {
VStack {
if let userProfile = userController.userProfile {
Text("\(userProfile.username)")
}
Button("Get data") {
Task {
await userController.getUserProfile()
}
}
Button("Update username") {
userController.updateUsername(newUsername: "HomeViewUsername")
}
}
}
}
}
struct ProfileView: View {
@Environment(UserController.self) var userController
var body: some View {
ScrollView {
VStack {
if let userProfile = userController.userProfile {
Text("\(userProfile.username)")
}
Button("Update username") {
userController.updateUsername(newUsername: "ProfileViewUsername")
}
}
}
}
}
Is there something obvious I’m missing? It does feel that I am missing something and I welcome any feedback so I can learn.




