Bart van Kuik
Bart van Kuik

Reputation: 4862

Triggering function before navigating in SwiftUI

My app shows a list of IDs. When you tap on the ID, a details view is opened, and the ID is used to retrieve details from the network. I'm currently doing this with an onAppear modifier, but I'm not happy with that. It's not really clean, and causes all sorts of other issues. I'd like to specifically trigger a function in the viewModel when the user navigates.

The following code can be pasted into a new SwiftUI project:

import SwiftUI

let fleet = Fleet(registries: ["NCC-1031"])

struct Fleet {
    let registries: [String]
}

struct Starship {
    let registry: String
    let name: String
}

class StarshipViewModel: ObservableObject {
    enum Mode {
        case idle
        case loading
        case success(Starship)
    }
    
    @Published var mode: Mode = .idle
    let registry: String
    private var timer: Timer?
    
    func fetchFromNetwork() {
        self.mode = .loading
        self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
            let starship = Starship(
                registry: "NCC-1031",
                name: "Discovery"
            )
            self.mode = .success(starship)
        })
    }
    
    init(registry: String) {
        self.registry = registry
    }
}

struct StarshipDetails: View {
    @ObservedObject var viewModel: StarshipViewModel
    
    var body: some View {
        VStack {
            switch self.viewModel.mode {
            case .idle:
                Text("Idle")
            case .loading:
                Text("Loading")
            case .success(let starship):
                Text("Name: \(starship.name)")
            }
        }
        .onAppear(perform: {
            viewModel.fetchFromNetwork()
        })
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                ForEach(fleet.registries, id: \.self) { registry in
                    NavigationLink(destination: self.makeDestination(from: registry)) {
                        Text(registry)
                    }
                }
            }
        }
    }
    
    private func makeDestination(from registry: String) -> StarshipDetails {
        let viewModel = StarshipViewModel(registry: registry)
        
        // Don't do it here, because a network request will be done for the whole list
//        viewModel.fetchFromNetwork()
        let view = StarshipDetails(viewModel: viewModel)
        return view
    }
}

How should I run the fetchFromNetwork() call when navigating, but without using onAppear?

Note: I can't just use a button with an action, because there's no way to get a reference to the viewModel.

Upvotes: 2

Views: 318

Answers (1)

Asperi
Asperi

Reputation: 257749

Actually, we can. It not needed any additional reference to viewModel.

Here is a demo of solution, similar to what was referenced before. Tested with Xcode 12.1 / iOS 14.1

demo

struct TestActionBeforeLink: View {
    @State private var navigate = false
    @State private var selectedRegistry: String = ""
    var body: some View {
        NavigationView {
            List {
                ForEach(fleet.registries, id: \.self) { registry in
                    Button(action: {
                        self.selectedRegistry = registry
                        self.navigate = true
                    }) {
                        HStack {
                            Text(registry)
                            Spacer()
                             Image(systemName: "chevron.right")   // if one needed
                        }
                    }
                }
            }
            .background(
                  NavigationLink(destination: self.makeDestination(from: selectedRegistry), isActive: $navigate) {
                        EmptyView()
                  }
            )
        }
    }
    
    // no changes below, just removed comment
    private func makeDestination(from registry: String) -> StarshipDetails {
        let viewModel = StarshipViewModel(registry: registry)
        
        let view = StarshipDetails(viewModel: viewModel)
        return view
    }
}

Upvotes: 1

Related Questions