SwiftUI - Как передать EnvironmentObject в View Model?

16

Я пытаюсь создать объект EnvironmentObject, к которому может обращаться модель представления (а не только представление).

Объект Environment отслеживает данные сеанса приложения, например, loggedIn, токен доступа и т. Д., Эти данные будут передаваться в модели представлений (или, при необходимости, классы обслуживания), чтобы позволить вызову API для передачи данных из этого EnvironmentObjects.

Я попытался передать объект сеанса инициализатору класса модели представления из представления, но получил ошибку.

Как я могу получить доступ / передать EnvironmentObject в модель представления с помощью SwiftUI?

Смотрите ссылку на тестовый проект: https://gofile.io/?c=vgHLVx

Майкл
источник
Почему бы не передать viewmodel как EO?
E.Coms
Кажется, что сверху будет много моделей представлений, ссылки, которые я
Майкл
2
Я не уверен, почему этот вопрос был отклонен, мне интересно то же самое. Я отвечу тем, что я сделал, надеюсь, кто-то еще может придумать что-нибудь получше.
Михаил Озерянский
2
@ E.Coms Я ожидал, что EnvironmentObject обычно будет одним объектом. Я знаю несколько работ, кажется, что запах кода делает их доступными для всех.
Михаил Озерянский
@ Майкл Ты даже нашел решение для этого?
Бретт

Ответы:

3

Я предпочитаю не иметь ViewModel. (Может быть, время для нового образца?)

Я настроил свой проект с RootViewнесколькими дочерними представлениями. Я насторить RootViewс Appобъектом как EnvironmentObject. Вместо того, чтобы ViewModel обращался к моделям, все мои представления обращаются к классам в App. Вместо ViewModel, определяющего макет, иерархия представления определяет макет. Сделав это на практике для нескольких приложений, я обнаружил, что мои взгляды остаются небольшими и конкретными. В качестве упрощения:

class App {
   @Published var user = User()

   let networkManager: NetworkManagerProtocol
   lazy var userService = UserService(networkManager: networkManager)

   init(networkManager: NetworkManagerProtocol) {
      self.networkManager = networkManager
   }

   convenience init() {
      self.init(networkManager: NetworkManager())
   }
}
struct RootView {
    @EnvironmentObject var app: App

    var body: some View {
        if !app.user.isLoggedIn {
            LoginView()
        } else {
            HomeView()
        }
    }
}
struct HomeView: View {
    @EnvironmentObject var app: App

    var body: some View {
       VStack {
          Text("User name: \(app.user.name)")
          Button(action: { app.userService.logout() }) {
             Text("Logout")
          }
       }
    }
}

В моих предварительных просмотрах я инициализирую a, MockAppкоторый является подклассом App. MockApp инициализирует назначенные инициализаторы с объектом Mocked. Здесь UserService не нужно подвергать насмешке, но источник данных (то есть NetworkManagerProtocol) делает это.

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            HomeView()
                .environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
        }
    }

}
Михаил Озерянский
источник
Просто примечание: я думаю, что лучше избегать цепочек, как app.userService.logout(). userServiceдолжен быть закрытым и доступным только из класса приложения. Приведенный выше код должен выглядеть следующим образом: Button(action: { app.logout() })и функция выхода из системы будет вызываться напрямую userService.logout().
pawello2222
@ pawello2222 Это не лучше, это просто рисунок фасада без какой-либо выгоды, но вы можете делать, как хотите.
Михаил Озерянский
3

Ты не должен. Это распространенное заблуждение, что SwiftUI лучше всего работает с MVVM.

MVVM нет места в SwfitUI. Вы спрашиваете, можете ли вы вставить прямоугольник в

соответствовать форме треугольника. Это не подходит.

Давайте начнем с некоторых фактов и будем работать шаг за шагом:

  1. ViewModel является моделью в MVVM.

  2. MVVM не принимает во внимание тип значения (например, такого нет в Java).

  3. Модель типа значения (модель без состояния) считается более безопасной, чем эталонная

    Типовая модель (модель с состоянием) в смысле неизменности.

Теперь MVVM требует от вас настроить модель таким образом, чтобы при каждом ее изменении она

обновляет вид некоторым заранее определенным способом. Это известно как связывание.

Без привязки у вас не будет хорошего разделения проблем, например; рефакторинг

модель и связанные с ней состояния и отделение их от вида.

Это две вещи, которые большинство разработчиков iOS MVVM терпят неудачу:

  1. В iOS нет «связующего» механизма в традиционном Java-смысле.

    Некоторые просто игнорируют привязку и думают, что вызывают объект ViewModel.

    автоматически все решает; некоторые введут Rx на основе KVO, и

    усложнить все, когда MVVM должен сделать вещи проще.

  2. модель с состоянием просто слишком опасна

    потому что MVVM уделяет слишком много внимания ViewModel, слишком мало - управлению состоянием

    и общие дисциплины в управлении контролем; большинство разработчиков в конечном итоге

    думая модель с состоянием, которое используется для обновления представления, можно использовать повторно и

    проверяемый .

    Вот почему Swift представляет тип значения в первую очередь; модель без

    штат.

Теперь к вашему вопросу: вы спрашиваете, может ли ваш ViewModel иметь доступ к EnvironmentObject (EO)?

Ты не должен. Потому что в SwiftUI модель, соответствующая View, автоматически имеет

ссылка на ЭО. Например;

struct Model: View {
    @EnvironmentObject state: State
    // automatic binding in body
    var body: some View {...}
}

Я надеюсь, что люди могут оценить, как компактный SDK разработан.

В SwiftUI MVVM работает автоматически . Нет необходимости в отдельном объекте ViewModel

это вручную связывает с просмотром, который требует ссылки EO, переданной ему.

Приведенный выше код является MVVM. Например; модель с привязкой к виду.

Но поскольку модель является типом значения, то вместо рефакторинга модели и состояния

Для просмотра модели вы реорганизуете управление (например, в расширении протокола).

Это официальный SDK, адаптирующий шаблон дизайна к языковой функции, а не просто

навязывая это. Приоритет содержания над формой.

Посмотрите на ваше решение, вы должны использовать синглтон, который в основном глобальный. Вы

должен знать, насколько опасно получать доступ к глобальным ресурсам без защиты

неизменность, которой у вас нет, потому что вы должны использовать модель ссылочного типа!

TL; DR

Вы не делаете MVVM Java-способом в SwiftUI. И Свифт-й способ сделать это не нужно

чтобы сделать это, это уже встроено.

Надеюсь, что больше разработчиков увидят это, так как это кажется популярным вопросом.

Джим Лай
источник
1

Ниже представлен подход, который работает для меня. Протестировано со многими решениями, начатыми с Xcode 11.1.

Проблема возникла из-за того, что EnvironmentObject вводится в общую схему.

SomeView().environmentObject(SomeEO())

то есть в первом созданном виде, во втором созданном объекте среды, в третьем объекте среды, введенном в вид

Таким образом, если мне нужно создать / настроить модель представления в конструкторе представления, то объекта среды там еще нет.

Решение: разбить все на части и использовать явное внедрение зависимостей

Вот как это выглядит в коде (общая схема)

// somewhere, say, in SceneDelegate

let someEO = SomeEO()                            // create environment object
let someVM = SomeVM(eo: someEO)                  // create view model
let someView = SomeView(vm: someVM)              // create view 
                   .environmentObject(someEO)

Здесь нет никакого компромисса, потому что ViewModel и EnvironmentObject по своему дизайну являются ссылочными типами (на самом деле ObservableObject), поэтому я передаю здесь и там только ссылки (так называемые указатели).

class SomeEO: ObservableObject {
}

class BaseVM: ObservableObject {
    let eo: SomeEO
    init(eo: SomeEO) {
       self.eo = eo
    }
}

class SomeVM: BaseVM {
}

class ChildVM: BaseVM {
}

struct SomeView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: SomeVM

    init(vm: SomeVM) {
       self.vm = vm
    }

    var body: some View {
        // environment object will be injected automatically if declared inside ChildView
        ChildView(vm: ChildVM(eo: self.eo)) 
    }
}

struct ChildView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: ChildVM

    init(vm: ChildVM) {
       self.vm = vm
    }

    var body: some View {
        Text("Just demo stub")
    }
}
Asperi
источник