UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

passing a navigationPath and using #Predicate inside a custom initialiser causes navigation link to freeze app

Forums > SwiftUI

I want to show a list of users that are saved through swift data. I want the user to be able to sort the list in a few options that I will provide or search for a particular user tapping on any of the users leads the user to a detailed view screen with further information since I want to provide more options later I want to be able to pass the navigation path to the detailed view I created a list view to run inside the initialiser my predicate and also passed a navigation path binding pressing the navigation link to the detailed view freezes the app if I remove the path or the predicate it works, but not together, here is the least code that reproduces the error struct ContentView: View {

@State var path = NavigationPath()

var body: some View {

    NavigationStack(path: $path) {

        ListView(path: $path)}

    }

}

struct ListView:View {

@Binding var path:NavigationPath

@Query var users:[User]

@Environment(\.modelContext) var modelContext

var body: some View {

    List(users) { user in

        NavigationLink("\(user.name)",value: user)

    }

    .task {

        // create some sample data

        if users.isEmpty == false { return }

        let user1 = User(name:"user1")

        let user2 = User(name: "user2")

        let user3 = User(name: "user3")

        modelContext.insert(user1)

        modelContext.insert(user2)

        modelContext.insert(user3)

    }

    .navigationDestination(for: User.self) { user in

        DetailedView(user:user,path: $path)

    }

}

init(path: Binding<NavigationPath>) {

    self._path = path

    self._users = Query(filter: #Predicate<User> { user in

        user.name.contains("2")

    })

}

}

struct DetailedView:View {

let user:User

@Binding var path:NavigationPath

var body: some View {

    Text("hello \(user.name)")

}

}

@Model class User {

var id = UUID()

var name:String

init(name: String) {

    self.name = name

}

}

   

I didn't really try to get into details, seems like it creates infinite loop with binding most probably as memory is used up and app basically freezes. One of the possible solutions create an observable object with nav path and inject into environment. So you'll be able to access it from the views which are actually need that object without passing down the subviews.

// Declare your observalbe class where you keep track of navigation
@Observable class NavPath {
    var path = NavigationPath()
}

struct ContentView: View {
    // Initialize object in root view
    @State var path = NavPath()

    var body: some View {
        NavigationStack(path: $path.path) {
            ListView()
        }
        // inject into environment to the view where you'll need access to the object
        .environment(path)
    }
}
// You won't need to pass to views the navPath where you actually don't use it at all
struct ListView: View {
    @Environment(\.modelContext) var modelContext
    @Query var users: [User]

    init() {
        let filter = #Predicate<User> { $0.name.contains("2") }
        _users = Query(filter: filter)
    }

    var body: some View {
        List(users) { user in
            NavigationLink("\(user.name)", value: user)
        }
        .onAppear {
            if users.isEmpty == false { return }
            let user1 = User(name: "user 1")
            let user2 = User(name: "user 2")
            let user3 = User(name: "user 3")
            modelContext.insert(user1)
            modelContext.insert(user2)
            modelContext.insert(user3)
        }
        .navigationDestination(for: User.self) { user in
            DetailedView(user: user)
        }
    }
}

// Now you can access it from the environment like so
struct DetailedView: View {
    let user: User
    @Environment(NavPath.self) var path

    var body: some View {
        Text("Hello \(user.name)")
    }
}

   

Yet another option to create path in the view where you already have filtered data like so:

struct ListView: View {
    @Environment(\.modelContext) var modelContext
    // create basically one view "lower" and pass it from here
    @State private var path = NavigationPath()
    @Query var users: [User]

    init() {
        let filter = #Predicate<User> { $0.name.contains("2") }
        _users = Query(filter: filter)
    }

    var body: some View {
        NavigationStack(path: $path) {
            List(users) { user in
                NavigationLink("\(user.name)", value: user)
            }
            .onAppear {
                if users.isEmpty == false { return }
                let user1 = User(name: "user 1")
                let user2 = User(name: "user 2")
                let user3 = User(name: "user 3")
                modelContext.insert(user1)
                modelContext.insert(user2)
                modelContext.insert(user3)
            }
            .navigationDestination(for: User.self) { user in
                DetailedView(user: user, path: $path)
            }
        }
    }
}

struct DetailedView: View {
    let user: User
    @Binding var path: NavigationPath

    var body: some View {
        Text("Hello \(user.name)")
    }
}

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire 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.