Меня смущает архитектура MVVM, особенно если учесть наличие ссылок внутри классов @Observable
. Рассмотрим следующий пример:
Предположим, я реализую приложение, которому требуется авторизация местоположения. Я хочу отобразить представление листа с запросом авторизации, если пользователь ее не предоставил. Для этого мне нужен какой-то делегат местоположения, который реагирует на изменение статуса авторизации. Источники ([1 ] [ 2]) обычно предлагают реализовать что-то вроде этого:
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
}
}
}
И рассмотрим простейший сценарий, в котором я должен реализовать представление, которое показывает лист, когда isAuthorized
имеет значение false. Мое «представление» и «модель представления» могут выглядеть примерно так:
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
}
}
Поправьте меня, если я ошибаюсь, но я не думаю, что изменения в isNotAuthorized
наблюдаются должным образом. Итак, я вижу несколько возможных решений:
Я помечаю LocationManager
как @Observable
, и в этом случае есть надежда, что isNotAuthorized
внутри ViewModel
будет пытаться ссылаться на опубликованную переменную внутри другого @Observable
класса. Я не уверен, сработает ли это и будет ли это хорошей идеей с точки зрения практики написания кода.
Я использую LocationManager
непосредственно внутри своего представления, и в этом случае я бы отказался от «модели представления» в архитектуре. Возможно, это нормально, поскольку MVVM — это всего лишь конструкция, но это означает, что LocationManager
теперь нужно будет управлять состояниями различных представлений. то есть мне понадобится еще одна переменная с именем isNotAuthorized
внутри LocationManager
. Когда существует несколько представлений и состояний просмотра, это становится беспорядком.
Я отказываюсь от любого типа управления состоянием внутри LocationManager
и помещаю все управления состояниями в «модель представления» каждого представления. Возможно, я могу добиться этого с помощью какого-то обратного вызова, но это кажется непрактичным. А именно, это означает, что любая модель, которую я реализую, не может хранить какое-либо состояние и должна предоставлять моделям представления способы обработки изменений состояния.
Отсюда мое замешательство. Такое ощущение, что я могу либо реализовать MVVM с вложенным наблюдением, либо заставить себя выполнять неоправданно сложное управление состоянием. Есть ли что-то простое, что мне не хватает?
В SwiftUI ваше представление — это ваша модель представления. Используйте @State
для типов значений, а не ссылочных типов. Я бы хотел, чтобы LocationManager
был @Observable
, и поместил его в окружающую среду. Тогда вы можете просто использовать его свойство isAuthorized
. Это вариант 2, но я не понимаю, зачем вам еще одно свойство. Вероятно, вам понадобится перечисление вместо логического значения, потому что вы можете иметь состояния «разрешено», «никогда не запрашивалось», «отказано» и «локация недоступна».
@Paulw11, что если мне нужно иметь несколько представлений, ссылающихся на LocationManager? И каждое представление должно иметь разный набор состояний? Например, представлению A нужна переменная isAuthorized, а представлению B нужен список изменений аутентификации?
@jnpdx да, в данном случае я мог бы, но я пытался донести, что здесь могут быть более сложные представления, например, список изменений аутентификации внутри LocationManager, но для представления требуется самое последнее, что-то вроде этого строки, в которых состояние, которое хочет представление, должно быть вычислено из другого состояния или нескольких состояний
Вот почему вы должны внедрить объект в окружающую среду; любое представление, которому нужен этот объект модели, может затем ссылаться на него. Если представлению требуется определенное свойство, вам необходимо добавить его в свою модель. Постоянные данные не могут существовать в модели представления, поскольку они эфемерны.
Чтобы стать «вещью» ViewModel, эта вещь должна предоставить способ публикации «свойств», которые будут отображаться одним связанным представлением, а также предоставить способ получения «команд», отправленных из представления, т.е. иметь функцию send(_: Event)
. Речь идет о ViewModel в MVVM. Все остальное — это детали реализации, то есть то, как вы на самом деле реализуете и интегрируете Location Manager, — это личное для ViewModel. На самом деле всю логику можно реализовать как одну статическую чистую функцию: (State, Event) -> State
. Эту функцию можно выполнить где угодно и как угодно.
Я думаю, что вам не хватает простой вещи: в SwiftUI структуры View
уже являются моделью представления. Попробуй это:
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 различает эти структуры модели представления и использует разницу для создания/инициализации/деинициализации объектов UIKit. Он выбирает разные объекты пользовательского интерфейса в зависимости от контекста и платформы. Он также обновляет их при изменении настроек региона, чего большинство никогда не забудет сделать в своих собственных моделях представлений.
«Мне понадобится еще одна переменная с именем isNotAuthorized внутри LocationManager». Почему? У вас уже есть один под названием
isAuthorized
. Почему бы просто не использовать это?