SwiftUI:如何在搜索栏的文本更改时触发 api 调用以检索数据源

SwiftUI: How to trigger api call to retrieve data source when Search Bar's text is changed

我使用 UIKit 实现了一个搜索栏,将其包装在 MovieSearchView 中。当用户输入一些关键字时,它将在以下列表视图中显示搜索结果。

数据 searchResults 来自 API 调用 self.model.getMovieSearchResults(),但在我的代码中,从未触发 api 调用。所以我想知道当搜索栏中的搜索文本发生更改时,我应该将条件检查 onAppear() 放在哪里以触发 api 调用?

/ / / 搜索视图

import SwiftUI

struct MovieSearchView: View {
    @ObservedObject var model = MovieListViewModel()
    @State private var searchText: String = ""

    var body: some View {
        NavigationView {
            VStack {
                SearchBar(text: $searchText)
                .onAppear() {
                    // If searchText has a value, then call api to fetch movies data.
                    if self.searchText.isEmpty == false {
                        self.model.getMovieSearchResults(for: self.searchText)
                    }
                }
                List {
                    ForEach(model.searchResults.filter {
                        self.searchText.isEmpty ? true : [=11=].title.lowercased().contains(self.searchText.lowercased())
                    }) { movie in
                        Text(movie.title)
                    }
                }
            }
        }
    }
}

struct MovieSearchView_Previews: PreviewProvider {
    static var previews: some View {
        MovieSearchView()
    }
}

/ / / 搜索栏

import SwiftUI

struct SearchBar: UIViewRepresentable {

    @Binding var text: String

    class Coordinator: NSObject, UISearchBarDelegate {

        @Binding var text: String

        init(text: Binding<String>) {
            _text = text
        }

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            text = searchText
        }
    }

    func makeCoordinator() -> SearchBar.Coordinator {
        return Coordinator(text: $text)
    }

    func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        searchBar.searchBarStyle = .minimal
        searchBar.autocapitalizationType = .none
        return searchBar
    }

    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
        uiView.text = text
    }
}

更新:

只是意识到我不需要再次进行过滤,因为 API 已经从服务器调用 return 过滤后的搜索结果。更改代码以移除过滤器{}。我还添加了点击手势以触发导航到电影详细信息视图并在您触底时调用第 2 页。

更多更新:

搜索栏

import SwiftUI

struct SearchBar: UIViewRepresentable {

    @Binding var text: String
    var onTextChanged: (String) -> Void

    class Coordinator: NSObject, UISearchBarDelegate {

        @Binding var text: String
        var onTextChanged: (String) -> Void

        init(text: Binding<String>, onTextChanged: @escaping (String) -> Void) {
            _text = text
            self.onTextChanged = onTextChanged
        }

        // Show cancel button when the user begins editing the search text
        func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
            searchBar.showsCancelButton = true
        }

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            text = searchText
            onTextChanged(text)
        }

        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            text = ""
            searchBar.showsCancelButton = false
            searchBar.endEditing(true)
            // Send back empty string text to search view, trigger self.model.searchResults.removeAll()
            onTextChanged(text)
        }
    }

    func makeCoordinator() -> SearchBar.Coordinator {
        return Coordinator(text: $text, onTextChanged: onTextChanged)
    }

    func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        searchBar.placeholder = "Search TMDB"
        searchBar.searchBarStyle = .minimal
        searchBar.autocapitalizationType = .none
        searchBar.showsCancelButton = true
        return searchBar
    }

    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
        uiView.text = text
    }
}

搜索视图

import SwiftUI

struct MovieSearchView: View {
    @ObservedObject var model = MovieListViewModel()
    @State private var searchText: String = ""
    @State private var selectedId = -1
    @State private var showSheet = false
    @State private var page = 1

    var body: some View {
        List {
            SearchBar(text: $searchText, onTextChanged: searchMovies)

            ForEach(0..<model.searchResults.count, id: \.self) { i in
                MovieListRow(movie: self.model.searchResults[i])
                    .onTapGesture {
                        self.selectedId = self.model.searchResults[i].id
                        self.showSheet.toggle()
                    }
                .onAppear() {
                    if i == self.model.searchResults.count - 1 {
                        self.page += 1
                        self.model.getMovieSearchResults(for: self.searchText, page: self.page)
                    }
                }
            }
        }
        .sheet(isPresented: $showSheet) {
            SingleMovieView(movieId: self.selectedId)
        }
    }

    func searchMovies(for searchText: String) {
        if !searchText.isEmpty {
            self.model.getMovieSearchResults(for: self.searchText, page: self.page)
        } else {
            // remove search result when a user clear keyword.
            self.model.searchResults.removeAll()
        }
    }
}

struct MovieSearchView_Previews: PreviewProvider {
    static var previews: some View {
        MovieSearchView()
    }
}

现在它在我的应用程序中是什么样子的。只剩下一件事,向下滚动列表时关闭键盘。

您需要使用如下的 searchBarDelegate 方法:

func searchBar(UISearchBar, shouldChangeTextIn: NSRange, replacementText: String) -> Bool

每次输入您都会收到来自用户的回调,在此函数中您可以触发对服务器的 api 调用。

您可以使用闭包。在这里,我将 onTextChanged: (String) -> Void 添加到搜索栏。

电影搜索视图

import SwiftUI

struct MovieSearchView: View {
    @ObservedObject var model = MovieListViewModel()
    @State private var searchText: String = ""

    var body: some View {
        NavigationView {
            VStack {
                SearchBar(text: $searchText, onTextChanged: searchMovies)
                
                List {
                    ForEach(model.searchResults.filter {
                        self.searchText.isEmpty ? true : [=10=].title.lowercased().contains(self.searchText.lowercased())
                    }) { movie in
                        Text(movie.title)
                    }
                }
            }
        }
    }
    
    func searchMovies(for searchText: String) {
        if !searchText.isEmpty {
            model.getMovieSearchResults(for: searchText)
        }
    }
}

struct MovieSearchView_Previews: PreviewProvider {
    static var previews: some View {
        MovieSearchView()
    }
}

搜索栏

import SwiftUI

struct SearchBar: UIViewRepresentable {
    @Binding var text: String
    var onTextChanged: (String) -> Void
    
    class Coordinator: NSObject, UISearchBarDelegate {
        var onTextChanged: (String) -> Void
        @Binding var text: String
        
        init(text: Binding<String>, onTextChanged: @escaping (String) -> Void) {
            _text = text
            self.onTextChanged = onTextChanged
        }
        
        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            text = searchText
            onTextChanged(text)
        }
    }
    
    func makeCoordinator() -> SearchBar.Coordinator {
        return Coordinator(text: $text, onTextChanged: onTextChanged)
    }
    
    func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        searchBar.searchBarStyle = .minimal
        searchBar.autocapitalizationType = .none
        return searchBar
    }
    
    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
        uiView.text = text
    }
}