I implement a search bar using UIKit to wrap it in MovieSearchView
. When a user input some keywords then it will show search result in the following list view.
The data searchResults
is coming from API call self.model.getMovieSearchResults()
, but in my code, the api call is never triggered. So I'm wondering where should I put the condition checking onAppear()
in my case to trigger api call, when the search text is changed in Search Bar?
/ / / Search View
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()
}
}
/ / / Search Bar
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
}
}
Just realize that I don't need to do the filter again, since API call already return the filtered search result from server. Change the code to remove the filter{}. I also add tap gesture to triggering navigation to movie detail view and call page 2 when you hit the bottom.
More update:
Clear search result when search text is cleared by the user.
Show cancel button on Search Bar when the user begin editing, and
dismiss keyboard when cancel button is pressed.
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()
}
}
Now what is it looked like in my app. Just one thing left, to dismiss the keyboard when scroll down the list.
You can use a closure. Here I added onTextChanged: (String) -> Void
to the 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
}
}