Bahn-X / swift-composable-navigator

An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind
MIT License
581 stars 25 forks source link

Initializer called multiple times #73

Closed onl1ner closed 2 years ago

onl1ner commented 3 years ago

Question

How to ensure that ViewModel would be initialized once?

Problem description

I am wondering what should I do to be sure that ViewModel object would be initialized once, because in my current implementation it doesn't work as I expect.

Here is my Screen:

struct TestScreen: Screen {
    var presentationStyle: ScreenPresentationStyle = .push

    struct Builder: NavigationTree {
        var builder: some PathBuilder {
            Screen(
                TestScreen.self,
                content: {
                    TestView(viewModel: .init()) // <- called multiple times.
                },
                nesting: {
                    TestSecondScreen.Builder()
                }
            )
        }
    }
}

So I changed implementation of Screen to:

struct TestScreen: Screen {
    var presentationStyle: ScreenPresentationStyle = .push

    struct Builder: NavigationTree {
        let viewModel: TestViewModel = .init()

        var builder: some PathBuilder {
            Screen(
                TestScreen.self,
                content: {
                    TestView(viewModel: viewModel) // <- called multiple times, but ViewModel is the same.
                },
                nesting: {
                    TestSecondScreen.Builder()
                }
            )
        }
    }
}

But when it comes to initialize ViewModel that expects data previous implementation won't work. Here is the example of what I have in that case:

struct TestSecondScreen: Screen {
    let title: String
    let id: String

    var presentationStyle: ScreenPresentationStyle = .push

    struct Builder: NavigationTree {
        var builder: some PathBuilder {
            Screen(
                content: { (screen: TestSecondScreen) in
                    TestSecondView(viewModel: .init(id: screen.id), title: screen.title)
                }
            )
        }
    }
}
ohitsdaniel commented 3 years ago

Hi @onl1ner!

Thank you for your question, you raise a very valid point. Both NavigationTree and PathBuilders are considered stateless, 'pure' structures that are re-initialised whenever needed. Therefore, NavigationTree cannot retain ViewModels, similar to SwiftUI Views. I would recommend, that you retain your ViewModel either in the View (as a @StateObject) or you hand it down the NavigationTree and retain it in your app root. As @StateObject solves this problem in iOS 14 and above, I decided not to invest too much time into this issue and keep it outside the library's domain.

If you have any other, open questions, let me know. Happy to help! :)

onl1ner commented 3 years ago

Thanks for your response and suggestions, as I understand there is no way to build MVVM module so the View wouldn't know about the data that is passed to ViewModel? Because if we are going to use @StateObject wrapper we have to initialize object at declaration, so there is no way to pass data to initializer, therefore it forces to create an initializer with no parameters and pass data, for example, in onAppear(perform:) function.

If so we will end up using @StateObject :)