Reputation: 4862
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
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
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