I'm confused by the MVVM architecture, especially when in consideration with having references inside of @Observable
classes. Consider the following example:
Suppose I'm implementing an app that needs location authorizations. I want to display a sheet view asking for the authorization if the user hasn't given it. For this, I need some kind of location delegate that responds to authorization status changes. Sources ([1] [2]) generally suggest implementing something like this:
final class LocationManager: NSObject {
static let shared = LocationManager()
private let locationManager = CLLocationManager()
var isAuthorized = false
override init() {
super.init()
locationManager.delegate = self
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ lm: CLLocationManager) {
// Respond to auth changes
switch locationManager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
isAuthorized = true
default:
isAuthorized = false
}
}
}
And consider the simplest scenario where I am to implement a view that shows a sheet when isAuthorized
is false. My "view" and "view-model" might look something like:
struct ContentView: View {
@State var viewModel = ViewModel()
var body: some View {
SomeOtherView()
.sheet(isPresented: $viewModel.isNotAuthorized) {
Text("Please authorize location services")
.interactiveDismissDisabled()
}
}
@Observable class ViewModel {
var isNotAuthorized = !LocationManager.shared.isAuthorized
}
}
Correct me if I'm wrong, but I don't think changes to isNotAuthorized
are properly observed. So I see a few possible solutions:
I mark LocationManager
itself as @Observable
, in which case the hope would be that isNotAuthorized
inside of ViewModel
would be trying to reference a published variable inside of another @Observable
class. I'm not sure if this would work, nor whether if it would be a good idea from a code practice perspective.
I use LocationManager
directly inside of my view, in which case I would be forgoing the "view-model" in the architecture. Perhaps this is fine since MVVM is just a construct, but this means that LocationManager
would now need to manage the states of different views. i.e. I would need to have another variable named isNotAuthorized
inside of LocationManager
. When there's multiple views and view states, it becomes a mess.
I forgo any type of state management inside of LocationManager
and push all state managements to every view's "view-model", I can perhaps achieve this with some sort of call back, but this seems impractical. Namely, this means for any model I implement, it cannot hold any state and must provide ways for view-models to process state changes.
Hence my confusion. It feels like I can either implement MVVM with nested observation or force myself to do unnecessarily complex state managements. Is there something simple I'm missing?
I think the simple thing you are missing is in SwiftUI the View
structs are a view model already. Try this:
struct ContentView: View {
var body: some View {
SomeOtherView()
.overlay {
AuthorizationView()
}
}
}
}
struct AuthorizationView: View {
@StateObject var locationAuthorizer = LocationAuthorizer()
var body: some View {
if !locationAuthorizer.isAuthorized) {
Text("Please authorize location services")
}
}
}
@Observable
final class LocationAuthorizer: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
var isAuthorized = false // @Published not needed because this class is @Observable
override init() {
super.init()
locationManager.delegate = self
}
func locationManagerDidChangeAuthorization(_ lm: CLLocationManager) {
// Respond to auth changes
switch locationManager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
isAuthorized = true
default:
isAuthorized = false
}
}
}
SwiftUI diffs these view model structs, uses the difference to create/init/deinit UIKit objects. It chooses different UI objects depending on the context and platform. It also updates them on change of region settings, which is something most would never remember to do in their own view models.