Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a SwiftUI TextField inside a navigation bar only accept input one character at a time

I want to allow the user to filter data in a long list to more easily find matching titles.

I have placed a TextView inside my navigation bar:

.navigationBarTitle(Text("Library"))
.navigationBarItems(trailing: TextField("search", text: $modelData.searchString)

I have an observable object which responds to changes in the search string:

class DataModel: ObservableObject {
   @Published var modelData: [PDFSummary]
   @Published var searchString = "" {
            didSet {
                if searchString == "" {
                    modelData =  Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name })
                } else {
                    modelData =  Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name }).filter({ $0.name.lowercased().contains(searchString.lowercased()) })
                }
            }
        }

Everything works fine, except I have to tap on the field after entering each letter. For some reason the focus is taken away from the field after each letter is entered (unless I tap on a suggested autocorrect - the whole string is correctly added to the string at once)

like image 790
Mozahler Avatar asked Apr 20 '20 14:04

Mozahler


People also ask

How does navigation work in SwiftUI?

Before NavigationStack and NavigationSplitView , SwiftUI introduced the NavigationView and NavigationLink structs. These two pieces work together to allow navigation between views in your application. The NavigationView is used to wrap the content of your views, setting them up for subsequent navigation.

How do I manage navigation in SwiftUI?

Getting a basic NavigationView with a title struct ContentView: View { var body: some View { NavigationView { Text("Hello, World!") } } } For simpler layouts navigation views should be the top-level thing in your view, but if you're using them inside a TabView then the navigation view should be inside the tab view.


1 Answers

The problem is in rebuilt NavigationView completely that result in dropped text field focus.

Here is working approach. Tested with Xcode 11.4 / iOS 13.4

The idea is to avoid rebuild NavigationView based on knowledge that SwiftUI engine updates only modified views, so using decomposition we make modifications local and transfer desired values only between subviews directly not affecting top NavigationView, as a result the last kept stand.

demo

class QueryModel: ObservableObject {
    @Published var query: String = ""
}

struct ContentView: View {
    // No QueryModel environment object here - 
    //                implicitly passed down. !!! MUST !!!

    var body: some View {
        NavigationView {
            ResultsView()
                .navigationBarTitle(Text("Library"))
                .navigationBarItems(trailing: SearchItem())
        }
    }
}

struct ResultsView: View {
    @EnvironmentObject var qm: QueryModel // << injected here from top
    var body: some View {
        VStack {
            Text("Search: \(qm.query)") // receive query string
        }
    }
}

struct SearchItem: View {
    @EnvironmentObject var qm: QueryModel // << injected here from top
    @State private var query = "" // updates only local view

    var body: some View {
        let text = Binding(get: { self.query }, set: {
            self.query = $0; self.qm.query = $0;       // transfer query string
        })
        return TextField("search", text: text)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(QueryModel())
    }
}
like image 161
Asperi Avatar answered Oct 22 '22 13:10

Asperi