NavigationStack with NavigationPath triggers multiple init/deinit of views in stack

I’ve stumbled upon what I think is a bug in SwiftUI, but there might be an explanation for it, which I cannot to figure out. So here’s the “bug”. Perhaps any of you know it.

In short
I found out that .navigationDestination(for: ...) runs multiple times when you abstract the logic away from the scope of the closure, into a function that should return the desired view from the type of destination. But if you keep the logic inside the scope of the closure it only runs once per destination.

The longer explanation
I’ve setup a small example of a setup, which I’m planning on building an entire app on. Basically it’s MVVVM with a coordinator pattern. I’ve ditched the coordinator in this example to minimize code, but the router is present and is key to how the app should work: navigation happens from a central place at the root of the app.

You should be able to copy all the code into a single file and run the app, to see what happens. It’s a simple app with a rootview that can navigate to either “Cookies” or “Milk”. You can set an amount (to have some sort of state, that you can test is still present, when navigating back to the view) + navigate on to another view.

The bug in question happens in the RootView:

.navigationDestination(for: Destination.self) { destination in
                let _ = print("||| Destination: (destination.rawValue)")
                // Method #1
//                anyViewFor(destination: destination)
                
                // Method #2
//                itemViewFor(destination: destination)
                
                // Method #3
//                cookieViewFor(destination: destination)
                
                // Method #4
                switch destination {
                case .cookie:
                    let vm = CookieViewModel()
                    CookieView(vm: vm)
                    
                case .milk:
                    let vm = MilkViewModel()
                    MilkView(vm: vm)
                }
            }

If you comment out Method 4 and comment in any of Method 1, 2, 3 you will see the issue in the console.

Say you navigate from RootView -> CookieView (Set 2 cookies) -> MilkView (Set 1 glass of milk) -> CookieView, and then navigate back to RootView.

Method 4 produces the following prints:

||| Router: add to navPath: 1
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 960, num: 0

||| Router: add to navPath: 2
||| Destination: Milk ????
||| Init ☀️: MilkViewModel, id: 254, num: 0

||| Router: add to navPath: 3
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 348, num: 0

||| Router: remove from navPath: 2
||| Deinit ????: CookieViewModel, id: 348, num: 0

||| Router: remove from navPath: 1
||| Deinit ????: MilkViewModel, id: 254, num: 1

||| Router: remove from navPath: 0
||| Deinit ????: CookieViewModel, id: 960, num: 2

This makes sense. The desired Views+ViewModels (we only have prints from VMs) are init’ed and deinit’ed.

Method 1, 2, 3 produces the following prints:

||| Router: add to navPath: 1
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 893, num: 0

||| Router: add to navPath: 2
||| Destination: Milk ????
||| Init ☀️: MilkViewModel, id: 747, num: 0
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 384, num: 0

||| Router: add to navPath: 3
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 578, num: 0
||| Destination: Milk ????
||| Init ☀️: MilkViewModel, id: 409, num: 0
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 468, num: 0
||| Deinit ????: CookieViewModel, id: 384, num: 0

||| Router: remove from navPath: 2
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 859, num: 0
||| Deinit ????: CookieViewModel, id: 468, num: 0
||| Destination: Milk ????
||| Init ☀️: MilkViewModel, id: 250, num: 0
||| Deinit ????: MilkViewModel, id: 409, num: 0
||| Deinit ????: CookieViewModel, id: 578, num: 0

||| Router: remove from navPath: 1
||| Destination: Cookie ????
||| Init ☀️: CookieViewModel, id: 211, num: 0
||| Deinit ????: CookieViewModel, id: 859, num: 0
||| Deinit ????: MilkViewModel, id: 250, num: 0
||| Deinit ????: MilkViewModel, id: 747, num: 1

||| Router: remove from navPath: 0
||| Deinit ????: CookieViewModel, id: 211, num: 0
||| Deinit ????: CookieViewModel, id: 893, num: 2

This is where it gets weird. When it’s a function returning the desired view for the given destination to .navigationDestination(for: ...) then it appears to be running n * number of items in the NavigationPath-object. You can see on the num: x in the deinit-prints, that instances are inited and deinted that we’re never in touch with.

Do any of you have a qualified guess why this is happening? To me it seems like a bug.

TESTABLE CODE:

public enum Destination: String, Codable, Hashable {
    case cookie = "Cookie ????"
    case milk = "Milk ????"
}
final class Router: ObservableObject {
    
    @Published var navPath = NavigationPath()
    
    func navigate(to destination: Destination) {
        navPath.append(destination)
        print("||| Router: add to navPath: (navPath.count)")
    }
    
    func navigateBack() {
        guard navPath.count > 0 else { return }
        navPath.removeLast()
        print("||| Router: remove from navPath: (navPath.count)")
    }
    
    func navigateToRoot() {
        guard navPath.count > 1 else { return }
        navPath.removeLast(navPath.count)
    }
    
}
struct RootView: View {
    
    @ObservedObject var router = Router()
    
    var body: some View {
        NavigationStack(path: $router.navPath) {
            List {
                Button(action: {
                    router.navigate(to: .cookie)
                }, label: {
                    Text(Destination.cookie.rawValue)
                })
                
                Button(action: {
                    router.navigate(to: .milk)
                }, label: {
                    Text(Destination.milk.rawValue)
                })
            }
            .navigationBarBackButtonHidden()
            .navigationDestination(for: Destination.self) { destination in
                let _ = print("||| Destination: (destination.rawValue)")
                // Method #1
//                anyViewFor(destination: destination)
                
                // Method #2
//                itemViewFor(destination: destination)
                
                // Method #3
//                cookieViewFor(destination: destination)
                
                // Method #4
                switch destination {
                case .cookie:
                    let vm = CookieViewModel()
                    CookieView(vm: vm)
                    
                case .milk:
                    let vm = MilkViewModel()
                    MilkView(vm: vm)
                }
            }
        }
        .environmentObject(router)
    }
    
    func anyViewFor(destination: Destination) -> AnyView {
        switch destination {
        case .cookie:
            let vm = CookieViewModel()
            return AnyView(CookieView(vm: vm))
            
        case .milk:
            let vm = MilkViewModel()
            return AnyView(MilkView(vm: vm))
        }
    }
    
    func itemViewFor(destination: Destination) -> ItemView {
        switch destination {
        case .cookie:
            let vm = CookieViewModel()
            let view = CookieView(vm: vm)
            let anyView = AnyView(view)
            return ItemView(childView: anyView)
            
        case .milk:
            let vm = MilkViewModel()
            let view = MilkView(vm: vm)
            let anyView = AnyView(view)
            return ItemView(childView: anyView)
        }
    }
    
    func cookieViewFor(destination: Destination) -> CookieView {
        switch destination {
        case .cookie:
            let vm = CookieViewModel()
            return CookieView(vm: vm)
        
        case .milk:
            let vm = CookieViewModel()
            return CookieView(vm: vm)
        }
    }
}
struct ItemView: View {
    
    var childView: AnyView
    
    var body: some View {
        childView
    }
}
struct CookieView: View {
    
    // MARK: Properties
    @EnvironmentObject var router: Router
    @StateObject var vm: CookieViewModel
    
    // MARK: - Views
    var body: some View {
        List {
            Stepper("Amount: (vm.amount)") {
                vm.incrementAmount()
            } onDecrement: {
                vm.decrementAmount()
            }
            .minimumScaleFactor(0.2)
            .padding(.top, 12)
            
            Button(action: {
                router.navigate(to: .milk)
            }, label: {
                Text("Get (Destination.milk.rawValue)")
            })
        }
        .navigationTitle(Destination.cookie.rawValue)
        .navigationBarBackButtonHidden()
        .toolbar(content: {
            ToolbarItem(placement: .topBarLeading) {
                Button(action: {
                    router.navigateBack()
                }, label: {
                    Text("Back")
                })
            }
        })
    }
}
class CookieViewModel: ObservableObject {
    
    @Published var amount: Int = 0
    
    let id: Int
    
    init() {
        self.id = Int.random(in: 1...1000)
        print("||| Init ☀️: CookieViewModel, id: (id), num: (amount)")
    }
    
    deinit {
        print("||| Deinit ????: CookieViewModel, id: (id), num: (amount)")
    }
    
    func incrementAmount() {
        amount += 1
    }
    
    func decrementAmount() {
        amount -= 1
    }
}
struct MilkView: View {
    
    // MARK: Properties
    @EnvironmentObject var router: Router
    @StateObject var vm: MilkViewModel
    
    // MARK: - Views
    var body: some View {
        List {
            Stepper("Amount: (vm.amount)") {
                vm.incrementAmount()
            } onDecrement: {
                vm.decrementAmount()
            }
            .minimumScaleFactor(0.2)
            .padding(.top, 12)
            
            Button(action: {
                router.navigate(to: .cookie)
            }, label: {
                Text("Get (Destination.cookie.rawValue)")
            })
        }
        .navigationTitle(Destination.milk.rawValue)
        .navigationBarBackButtonHidden()
        .toolbar(content: {
            ToolbarItem(placement: .topBarLeading) {
                Button(action: {
                    router.navigateBack()
                }, label: {
                    Text("Back")
                })
            }
        })
    }
}
class MilkViewModel: ObservableObject {
    
    @Published var amount: Int = 0
    
    let id: Int
    
    init() {
        self.id = Int.random(in: 1...1000)
        print("||| Init ☀️: MilkViewModel, id: (id), num: (amount)")
    }
    
    deinit {
        print("||| Deinit ????: MilkViewModel, id: (id), num: (amount)")
    }
    
    func incrementAmount() {
        amount += 1
    }
    
    func decrementAmount() {
        amount -= 1
    }
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật