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

3
我使用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 : $0.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调用已经从服务器返回了过滤后的搜索结果。修改代码以删除filter{}。 我还添加了轻击手势来触发导航到电影详细视图,并在到达底部时调用第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()
    }
}

现在看起来在我的应用程序中是什么样子。只剩下一件事情,当滚动列表向下时关闭键盘。 enter image description here

2个回答

1
你可以使用闭包。在这里我添加了onTextChanged: (String) -> Void到SearchBar中。

电影搜索视图

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 : $0.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
    }
}

我尝试了一下,它运行得很好。据我理解,这个解决方案将闭包(包括API调用语句)分配给SearchBar。在SearchBar内部,它会回调在MovieSearchView中定义的函数,非常有趣。所以我们不需要在SearchBar中调用模型,只需发送一个闭包即可。我说的对吗? - Zhou Haibo
是的,SearchBar 可以在其他视图中使用,因为它不需要引用您的模型即可工作。它只知道必须使用 String 参数调用 onTextChanged - Nicolas Mandica
谢谢你的回答,真的帮了我很多。我更新了我的问题代码,因为我刚刚意识到我做了两次过滤工作,从API调用返回的数据已经包含了过滤结果。我真是个笨蛋 :) - Zhou Haibo

0

你需要像下面这样使用searchBarDelegate方法:

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

每次用户输入时,您都会收到一个回调,您可以在此函数内部触发对服务器的 API 调用。

谢谢您的回答,我已经尝试过了。但是结果是,在添加了这个函数并调用API后,我无法在搜索栏中输入任何单词。 - Zhou Haibo
您可以添加与文本计数相关的检查。 - Anil Kumar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接