8
votes

Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:

  1. RootView may be updated by some kind of global event e.g. missed internet connection or by it's own data/view model
  2. When RootView handling event it's content is updated and this is causes new struct of DetailView to be created
  3. If DetailViewModel is created by DetailView on init, there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed

How can we avoid this situation?

  1. Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
  2. Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
  3. Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing @States what is forbidden by SwiftUI
  4. Better approach?

Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.

For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.

Update:

Lets add some code to see whats happening if VM is created by it's 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()
}

Update 2:

It seems that if we store ObservableObject as @State in view (not as ObservedObject) View keeps reference on VM

@State var viewModel = ContentDetailViewModel()

Any negative effects? Can we use it like this?

Update 3:

It seems that if ViewModel kept in View's @State:

  1. and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
  2. and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same

Mehhh...

Update 4:

This approach also allows you to keep strong references in bindings closures

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
I guess, you should consider EnvironmentObject approach where your RootView will have AppState object and all his children will have an access to this, so 1) you still can update AppState by some global event 2) RootView recreate DetailView when update is occurred 3) DetailView will create DetailViewModel from @EnvironmentObject appState object. - Tikhonov Aleksandr
Keen to follow findings here. I’ve come across the same issues you face with no idea on best implantation for a single view use case that’s architecture agnostic! - ryannn
@ryannn in fact I ended up with idea how it works in Flutter. Just keep it all in EnvironmentObject or EnvironmentValues, basiclly they are all singltons with ability to be overriden in particular View and they are all representing application state. I suggest you to look at Google videos about app design in Flutter like this one youtube.com/watch?v=d_m5csmrf7I - Lex

1 Answers

1
votes

Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.

It looks like a single state container fits best for SwiftUI architecture.

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)
 }
}

You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.

I wrote a blog post about this approach, take a look at it for more information https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/