GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

NavigationStackView DetailView not reseting States and StateObjects when changing selection

Forums > SwiftUI

Hello guys!

I am scratching my head with the following:)

I have a 3 column NavigationSplitView where a detailView takes a flight which is a Core Data model like so (minimal reproducible code):

import SwiftUI

enum SideBarMenuCategory: Int, CaseIterable, Identifiable {
    case flights
    var id: Int { rawValue }
}

class FlightModel: ObservableObject {
    let id: UUID
    let text: String

    init(text: String) {
        self.id = UUID()
        self.text = text
    }
}

class RouteManager: ObservableObject {
    @Published var selectedCategory: SideBarMenuCategory? = .flights
    @Published var visibility: NavigationSplitViewVisibility = .automatic
    @Published var selectedFlight: FlightModel? = nil

    @Published var selectedFlights = Set<UUID>() {
        didSet {
            updateSelectedFlight()
        }
    }

    let flightStore = FlightStore.instance

    func updateSelectedFlight() {
        guard selectedFlights.count == 1,
              let flightID = selectedFlights.first
        else {
            selectedFlight = nil
            return
        }
        selectedFlight = flightStore.findFlight(by: flightID)
    }
}

class FlightStore: ObservableObject {
    static let instance = FlightStore()

    private init() {}

    @Published var flights: [FlightModel] = [FlightModel(text: "Test"), FlightModel(text: "Test 2")]

    func findFlight(by id: UUID?) -> FlightModel? {
        flights.first { $0.id == id }
    }
}

struct SidebarView: View {
    @Binding var selectedCategory: SideBarMenuCategory?

    var body: some View {
        List(selection: $selectedCategory) {
            NavigationLink(value: SideBarMenuCategory.flights) {
                Label("Flights", systemImage: "book")
            }
        }
    }
}

struct FlightsOverView: View {
    @EnvironmentObject var routeManager: RouteManager
    @EnvironmentObject var flightStore: FlightStore

    var body: some View {
        List(flightStore.flights, id: \.id, selection: $routeManager.selectedFlights) { flight in
            HStack {
                Text(flight.id.uuidString)
                Spacer()
                Text(flight.text)
            }
            .tag(flight.id)
        }
    }
}

struct SplitView: View {
    @StateObject var routeManager = RouteManager()
    @StateObject var flightStore = FlightStore.instance

    var body: some View {
        NavigationSplitView(columnVisibility: $routeManager.visibility) {
            SidebarView(selectedCategory: $routeManager.selectedCategory)
                .navigationTitle("Menu")
        } content: {
            SecondColumnView()
        } detail: {
            ThirdColumnView()
        }
        .environmentObject(routeManager)
        .environmentObject(flightStore)
    }
}

struct SecondColumnView: View {
    @EnvironmentObject var routeManager: RouteManager

    var body: some View {
        if let selectedCategory = routeManager.selectedCategory {
            switch selectedCategory {
            case .flights:
                FlightsOverView()
            }
        } else {
            EmptyView()
        }
    }
}

struct ThirdColumnView: View {
    @EnvironmentObject var routeManager: RouteManager

    var body: some View {
        if let category = routeManager.selectedCategory {
            switch category {
            case .flights:
                if let flight = routeManager.selectedFlight {
                    FlightDetailView(flight: flight)
                }
            }
        } else {
            Text("Select View")
        }
    }
}

class DetailVM: ObservableObject {
    init() {
        print("DetailVM is Init")
    }

    deinit {
        print("DetailVM is Deinit")
    }
}

struct FlightDetailView: View {
    @ObservedObject var flight: FlightModel
    @StateObject var detailVM = DetailVM()

    @State var isOn = false

    var body: some View {
        VStack(alignment: .center) {
            Text(flight.text)
            Toggle("", isOn: $isOn)
        }
        .task {
            print("I want to set something up")
        }
        .onDisappear {
            print("I want to perform some checks")
        }
    }
}

FlightsOverView holds List with selection. That selection returns a Set<UUID> which I filter and return a selectedFlight. The change takes place as expected and view updates. However, all @StateObject and @State held by FlightDetailView do not reset when changing selection (.task runs exactly once on the initial selection and .onDisappear is never invoked). I am migrating from NavigationStack where this isn't an issue as the view disappears before another one can be selected. How could this be fixed in NavigationSplitView? I tried assigning a .id(flight.id) to the view but this only causes in @StateObjects of that view to init each time flight selection is changed but no deinit takes place.

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's all new Paywall Editor allow you to remotely configure your paywall view without any code changes or app updates.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.