Сохранять ссылку на модель представления / данных после обновления представления

Считайте, что у нас есть RootView и DetailView. DetailView есть собственный BindableObject, назовем его DetailViewModel и у нас есть сценарий:

  1. RootView может быть обновлено каким-то глобальным событием, например, пропущенным интернет-соединением или собственной моделью данных / представления
  2. когда RootView обрабатывая событие, его содержимое обновляется, и это вызывает новую структуру DetailView быть созданным
  3. Если DetailViewModel создан DetailView при инициализации будет еще одна ссылка на DetailViewModel и его состояние (например, выбранный объект) будет пропущено

Как избежать этой ситуации?

  1. Храните все модели ViewModels как EnvironmentObjects, которые в основном представляют собой одноэлементный пул. Такой подход приводит к тому, что ненужные объекты хранятся в памяти, когда они не используются.
  2. Передайте все модели ViewModels из RootView дочерним элементам и дочерним элементам дочернего элемента (с указанными выше недостатками + болезненные зависимости)
  3. Храните независимые от представления объекты DataObject (также известные как рабочие) как EnvironmentObjects. В таком случае, где мы храним зависимые от представления состояния, соответствующие модели? Если мы сохраним его в View, он окажется в ситуации, когда мы перекрестно меняем @States, что запрещено SwiftUI.
  4. Лучше подход?

Извините за то, что не предоставил код. Этот вопрос касается концепции архитектуры Swift UI, где мы пытаемся объединить декларативные структуры и ссылочные объекты с данными.

На данный момент я не вижу способа сохранить ссылки, которые соответствуют только соответствующему представлению, и не хранить их в памяти / среде навсегда в их текущих состояниях.

Обновить:

Давайте добавим код, чтобы увидеть, что происходит, если виртуальная машина создана с помощью View

import SwiftUI
import Combine

let trigger = Timer.publish(every: 2.0, on: .main, in: .default)

struct ContentView: View {

    @State var state: Date = Date()

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: ContentDetailView(), label: {
                    Text("Navigation push")
                        .padding()
                        .background(Color.orange)
                })
                Text("\(state)")
                    .padding()
                    .background(Color.green)
                ContentDetailView()
            }
        }
        .onAppear {
            _ = trigger.connect()
        }
        .onReceive(trigger) { (date) in
            self.state = date
        }
    }
}

struct ContentDetailView: View {

    @ObservedObject var viewModel = ContentDetailViewModel()
    @State var once = false

    var body: some View {
        let vmdesc = "View model uuid:\n\(viewModel.uuid)"
        print("State of once: \(once)")
        print(vmdesc)
        return Text(vmdesc)
            .multilineTextAlignment(.center)
            .padding()
            .background(Color.blue)
            .onAppear {
                self.once = true
            }
    }
}

class ContentDetailViewModel: ObservableObject, Identifiable {
    let uuid = UUID()
}

Обновление 2:

Кажется, что если мы сохраняем ObservableObject как @State в представлении (а не как ObservedObject), View сохраняет ссылку на виртуальную машину

@State var viewModel = ContentDetailViewModel()

Есть отрицательные эффекты? Можем ли мы использовать это так?

Обновление 3:

Кажется, что если ViewModel хранится в View @State:

  1. и ViewModel сохраняется путем закрытия с сильной ссылкой - deinit никогда не будет выполнен -> утечка памяти
  2. и ViewModel сохраняется закрытием со слабой ссылкой - deinit вызывает каждый раз при обновлении представления, все subs будут сброшены, но свойства останутся прежними

Ммм...

Обновление 4:

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

import Foundation
import Combine
import SwiftUI

/**
 static func instanceInView() -> UIViewController {
     let vm = ContentViewModel()
     let vc = UIHostingController(rootView: ContentView(viewModel: vm))
     vm.bind(uiViewController: vc)
     return vc
 }
 */
public protocol ViewModelProtocol: class {
    static func instanceInView() -> UIViewController
    var bindings: Set<AnyCancellable> { get set }
    func onAppear()
    func onDisappear()
}

extension ViewModelProtocol {

    func bind(uiViewController: UIViewController) {
        uiViewController.publisher(for: \.parent)
            .sink(receiveValue: { [weak self] (parent) in
                if parent == nil {
                    self?.bindings.cancel()
                }
            })
            .store(in: &bindings)
    }

}

struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {

    func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
        return ViewModel.instanceInView()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
        //
    }
}
struct RootView: View {

    var body: some View {
        ModelView<ParkingViewModel>()
            .edgesIgnoringSafeArea(.vertical)
    }

}

1 ответ

Apple говорит, что мы должны использовать ObservableObject для данных, которые находятся за пределами SwiftUI. Это означает, что вы должны сами управлять своим источником данных.

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

typealias Reducer<State, Action> = (inout State, Action) -> Void

final class Store<State, Action>: ObservableObject {
 @Published private(set) var state: State

 private let reducer: Reducer<State, Action>

 init(initialState: State, reducer: @escaping Reducer<State, Action>) {
     self.state = initialState
     self.reducer = reducer
 }

 func send(_ action: Action) {
     reducer(&state, action)
 }
}

Вы можете передать экземпляр магазина в среду вашего приложения SwiftUI, и он будет доступен во всех представлениях и сохранит состояние вашего приложения без потери данных.

Я написал сообщение в блоге об этом подходе, взгляните на него для получения дополнительной информации https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/

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