John O'Reilly
John O'Reilly

Reputation: 10330

TabView lifecycle issue if views are the same

I have TabView containing 2 entries....both trigger display of StationListView. The issue I have is that when I click on 2nd tab, the onAppear is being called twice (once each for the 2 instances) having the effect of data for 2nd tab showing, following shortly by data again for 1st tab.

struct ContentView : View {
    @ObservedObject var cityBikesViewModel = CityBikesViewModel(repository: CityBikesRepository())
    @State private var selection = 0
    
    var body: some View {
        TabView(selection: $selection) {
            StationListView(cityBikesViewModel: cityBikesViewModel, network: "galway")
                .tabItem {
                    VStack {
                        Image(systemName: "location")
                        Text("Galway")
                    }
                }.tag(0)
            StationListView(cityBikesViewModel: cityBikesViewModel, network: "oslo-bysykkel")
                .tabItem {
                    VStack {
                        Image(systemName: "location")
                        Text("Oslo")
                    }
                }.tag(1)
        }
    }
}

struct StationListView: View {
    @ObservedObject var cityBikesViewModel : CityBikesViewModel
    var network: String
 
    var body: some View {
        NavigationView {
            List(cityBikesViewModel.stationList, id: \.id) { station in
                StationView(station: station)
            }
            .navigationBarTitle(Text("Bike Share"))
            .onAppear(perform: {
                self.cityBikesViewModel.fetch(network: self.network)
            })
        }
    }
}

Full code at https://github.com/joreilly/BikeShare/blob/master/ios/BikeShare/BikeShare/ContentView.swift

Upvotes: 0

Views: 371

Answers (1)

vacawama
vacawama

Reputation: 154533

You'd like the view to reload the data from the network when it is selected. Unfortunately, .onAppear doesn't work because the View isn't loaded when you select it.

Instead, reload the data when selection changes.

To make this work in SwiftUI 2.0 (Xcode 12), pass the tag number and selection (as a Binding) to the StationListView, then have StationListView reload the data when the tag matches the selection both .onAppear and when selection changes using onChange(of: selection):

struct StationListView: View {
    @ObservedObject var cityBikesViewModel : CityBikesViewModel
    var network: String
    var tag: Int
    @Binding var selection: Int
 
    var body: some View {
        NavigationView {
            List(cityBikesViewModel.stationList, id: \.id) { station in
                StationView(station: station)
            }
            .navigationBarTitle(Text("Bike Share"))
            .onChange(of: selection) { _ in
                refreshData()
            }
            .onAppear {
                refreshData()
            }
        }
    }
    
    func refreshData() {
        if tag == selection {
            cityBikesViewModel.fetch(network: self.network)
        }
    }
}

Here is the TabView:

TabView(selection: $selection) {
    StationListView(cityBikesViewModel: cityBikesViewModel, network: "galway", tag: 0, selection: $selection)
        .tabItem {
            VStack {
                Image(systemName: "location")
                Text("Galway")
            }
        }.tag(0)
    StationListView(cityBikesViewModel: cityBikesViewModel, network: "oslo-bysykkel", tag: 1, selection: $selection)
        .tabItem {
            VStack {
                Image(systemName: "location")
                Text("Oslo")
            }
        }.tag(1)
    }
}

Upvotes: 1

Related Questions