MVVM и ответственность государственного управления

Меня смущает архитектура 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 наблюдаются должным образом. Итак, я вижу несколько возможных решений:

  1. Я помечаю LocationManager как @Observable, и в этом случае есть надежда, что isNotAuthorized внутри ViewModel будет пытаться ссылаться на опубликованную переменную внутри другого @Observable класса. Я не уверен, сработает ли это и будет ли это хорошей идеей с точки зрения практики написания кода.

  2. Я использую LocationManager непосредственно внутри своего представления, и в этом случае я бы отказался от «модели представления» в архитектуре. Возможно, это нормально, поскольку MVVM — это всего лишь конструкция, но это означает, что LocationManager теперь нужно будет управлять состояниями различных представлений. то есть мне понадобится еще одна переменная с именем isNotAuthorized внутри LocationManager. Когда существует несколько представлений и состояний просмотра, это становится беспорядком.

  3. Я отказываюсь от любого типа управления состоянием внутри LocationManager и помещаю все управления состояниями в «модель представления» каждого представления. Возможно, я могу добиться этого с помощью какого-то обратного вызова, но это кажется непрактичным. А именно, это означает, что любая модель, которую я реализую, не может хранить какое-либо состояние и должна предоставлять моделям представления способы обработки изменений состояния.

Отсюда мое замешательство. Такое ощущение, что я могу либо реализовать MVVM с вложенным наблюдением, либо заставить себя выполнять неоправданно сложное управление состоянием. Есть ли что-то простое, что мне не хватает?

«Мне понадобится еще одна переменная с именем isNotAuthorized внутри LocationManager». Почему? У вас уже есть один под названием isAuthorized. Почему бы просто не использовать это?

jnpdx 30.05.2024 23:26

В SwiftUI ваше представление — это ваша модель представления. Используйте @State для типов значений, а не ссылочных типов. Я бы хотел, чтобы LocationManager был @Observable, и поместил его в окружающую среду. Тогда вы можете просто использовать его свойство isAuthorized. Это вариант 2, но я не понимаю, зачем вам еще одно свойство. Вероятно, вам понадобится перечисление вместо логического значения, потому что вы можете иметь состояния «разрешено», «никогда не запрашивалось», «отказано» и «локация недоступна».

Paulw11 30.05.2024 23:26

@Paulw11, что если мне нужно иметь несколько представлений, ссылающихся на LocationManager? И каждое представление должно иметь разный набор состояний? Например, представлению A нужна переменная isAuthorized, а представлению B нужен список изменений аутентификации?

bli00 31.05.2024 00:20

@jnpdx да, в данном случае я мог бы, но я пытался донести, что здесь могут быть более сложные представления, например, список изменений аутентификации внутри LocationManager, но для представления требуется самое последнее, что-то вроде этого строки, в которых состояние, которое хочет представление, должно быть вычислено из другого состояния или нескольких состояний

bli00 31.05.2024 00:49

Вот почему вы должны внедрить объект в окружающую среду; любое представление, которому нужен этот объект модели, может затем ссылаться на него. Если представлению требуется определенное свойство, вам необходимо добавить его в свою модель. Постоянные данные не могут существовать в модели представления, поскольку они эфемерны.

Paulw11 31.05.2024 02:28

Чтобы стать «вещью» ViewModel, эта вещь должна предоставить способ публикации «свойств», которые будут отображаться одним связанным представлением, а также предоставить способ получения «команд», отправленных из представления, т.е. иметь функцию send(_: Event). Речь идет о ViewModel в MVVM. Все остальное — это детали реализации, то есть то, как вы на самом деле реализуете и интегрируете Location Manager, — это личное для ViewModel. На самом деле всю логику можно реализовать как одну статическую чистую функцию: (State, Event) -> State. Эту функцию можно выполнить где угодно и как угодно.

CouchDeveloper 31.05.2024 10:35
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я думаю, что вам не хватает простой вещи: в 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. Он выбирает разные объекты пользовательского интерфейса в зависимости от контекста и платформы. Он также обновляет их при изменении настроек региона, чего большинство никогда не забудет сделать в своих собственных моделях представлений.

Другие вопросы по теме