Сохранять ссылку на модель представления / данных после обновления представления
Считайте, что у нас есть RootView
и DetailView
. DetailView
есть собственный BindableObject, назовем его DetailViewModel
и у нас есть сценарий:
RootView
может быть обновлено каким-то глобальным событием, например, пропущенным интернет-соединением или собственной моделью данных / представления- когда
RootView
обрабатывая событие, его содержимое обновляется, и это вызывает новую структуруDetailView
быть созданным - Если
DetailViewModel
созданDetailView
при инициализации будет еще одна ссылка наDetailViewModel
и его состояние (например, выбранный объект) будет пропущено
Как избежать этой ситуации?
- Храните все модели ViewModels как EnvironmentObjects, которые в основном представляют собой одноэлементный пул. Такой подход приводит к тому, что ненужные объекты хранятся в памяти, когда они не используются.
- Передайте все модели ViewModels из RootView дочерним элементам и дочерним элементам дочернего элемента (с указанными выше недостатками + болезненные зависимости)
- Храните независимые от представления объекты DataObject (также известные как рабочие) как EnvironmentObjects. В таком случае, где мы храним зависимые от представления состояния, соответствующие модели? Если мы сохраним его в View, он окажется в ситуации, когда мы перекрестно меняем @States, что запрещено SwiftUI.
- Лучше подход?
Извините за то, что не предоставил код. Этот вопрос касается концепции архитектуры 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:
- и ViewModel сохраняется путем закрытия с сильной ссылкой - deinit никогда не будет выполнен -> утечка памяти
- и 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/