SwiftUI @Binding reloading on push/pop with different navigation items

Viewed 241

I've got a very simple app example that has two views: a MasterView and a DetailView. The MasterView is presented inside a ContentView with a NavigationView:

import SwiftUI

struct ContentView: View {

    var body: some View {
        NavigationView {
            MasterView(viewModel: MasterViewModel())
                .navigationBarTitle(Text("Master"))
                .navigationBarItems(
                    leading: EditButton()
                )
        }
    }
}

struct MasterView: View {

    @ObservedObject private var viewModel: MasterViewModel

    init(viewModel: MasterViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        print("Test")
        return DataStatusView(dataSource: self.$viewModel.result) { texts -> AnyView in
            print("Closure")
            return AnyView(List {
                ForEach(texts, id: \.self) { text in
                    NavigationLink(
                        destination: DetailView(viewModel: DetailViewModel(stringToDisplay: text))
                    ) {
                        Text(text)
                    }
                }
            })
        }.onAppear {
            if case .waiting = self.viewModel.result {
                self.viewModel.fetch()
            }
        }
    }
}

struct DetailView: View {
    @ObservedObject private var viewModel: DetailViewModel


    init(viewModel: DetailViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        self.showView().onAppear {
            self.viewModel.fetch()
        }
        .navigationBarTitle(Text("Detail"))
    }

    func showView() -> some View {
        switch self.viewModel.result {
        case .found(let s):
            return AnyView(Text(s))
        default:
            return AnyView(Color.red)
        }
    }
}

The DataStatusView is a simple view to manage some state:


public enum ResultState<T, E: Error> {
    case waiting
    case loading
    case found(T)
    case failed(E)
}
struct DataStatusView<Content, T>: View where Content: View {

    @Binding private(set) var dataSource: ResultState<T, Error>
    private let content: (T) -> Content
    private let waitingContent: AnyView?

    @inlinable init(dataSource: Binding<ResultState<T, Error>>,
                    waitingContent: AnyView? = nil,
                    @ViewBuilder content: @escaping (T) -> Content) {
        self._dataSource = dataSource
        self.waitingContent = waitingContent
        self.content = content
    }

    var body: some View {
        self.buildMainView()
    }

    private func buildMainView() -> some View {
        switch self.dataSource {
        case .waiting:
            return AnyView(Color.red)
        case .loading:
            return AnyView(Color.green)
        case .found(let data):
            return AnyView(self.content(data))
        case .failed:
            return AnyView(Color.yellow)
        }
    }
}

and the view models are a very simple "pretend to make a network call" vm:

final class MasterViewModel: ObservableObject {

    @Published var result: ResultState<[String], Error> = .waiting

    init() { }

    func fetch() {
        self.result = .loading
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
            guard let self = self else { return }
            self.result = .found(["This", "is", "a", "test"])
        }
    }
}

final class DetailViewModel: ObservableObject {

    @Published var result: ResultState<String, Error> = .waiting

    private let stringToDisplay: String
    init(stringToDisplay: String) {
        self.stringToDisplay = stringToDisplay
    }

    func fetch() {
        self.result = .loading
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
            guard let self = self else { return }
            self.result = .found(self.stringToDisplay)
        }
    }
}

Now the problem I'm having is that every time I go from Master -> Detail view the block inside the DataStatusView is called. This is a problem because the "DetailView" is constantly re-created (and therefore its vm too, which causes the loading of the detail's data to fail).

This is happening because when I go from master -> detail the buttons in the navigation bar change (or at least that's the hypothesis). When I remove the lines:

.navigationBarItems(
                    leading: EditButton()
                )

This works as "expected".

What is the "SwiftUI" way of dealing with this? A sample project that shows this issue is here: https://github.com/kerrmarin/swiftui-mvvm-master-detail

0 Answers
Related