I have a search bar inside a view where I can search and the search will be passed to a REST api and the result will be showed on a tableView. Below are my different classes
Model:
struct MovieResponse: Codable {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(totalResults: Int, response: String, error: String, movies: [Movie]) {
self.totalResults = totalResults
self.response = response
self.error = error
self.movies = movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults) ?? 0
self.response = try values.decodeIfPresent(String.self, forKey: .response) ?? "False"
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies) ?? []
}
}
extension MovieResponse {
struct Movie: Codable, Identifiable {
var id = UUID()
var title: String
var year: Int8
var imdbID: String
var type: String
var poster: URL
enum EncodingKeys: String, CodingKey {
case title = "Title"
case year = "Year"
case imdmID
case type = "Type"
case poster = "Poster"
}
}
}
ViewModel:
final class MovieListViewModel: ObservableObject {
@Published var isLoading: Bool = false
@Published var movieObj = MovieResponse(totalResults: 0, response: "False", error: "", movies: [])
var searchTerm: String = ""
private let searchTappedSubject = PassthroughSubject<Void, Error>()
private var disposeBag = Set<AnyCancellable>()
private let service = OMDBService()
init() {
searchTappedSubject
.flatMap {
self.requestMovies(searchTerm: self.searchTerm)
.handleEvents(receiveSubscription: { _ in
DispatchQueue.main.async {
self.isLoading = true
}
},
receiveCompletion: { comp in
DispatchQueue.main.async {
self.isLoading = false
}
})
.eraseToAnyPublisher()
}
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.assign(to: \.movieObj.movies, on: self)
.store(in: &disposeBag)
}
func onSearchTapped() {
searchTappedSubject.send(())
}
private func requestMovies(searchTerm: String) -> AnyPublisher<[MovieResponse.Movie], Error> {
guard let url = URL(string:"\(Constants.HostName)/?s=\(searchTerm)&apikey=\(Constants.APIKey)") else {
fatalError("Something is wrong with URL")
}
return URLSession.shared.dataTaskPublisher(for: url)
.tryMap() { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return element.data
}
.mapError { $0 as Error }
.decode(type: [MovieResponse.Movie].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
And finally, the view & search bar
struct SearchView: View {
@ObservedObject var viewModel = MovieListViewModel()
@State private var searchText = ""
var body: some View {
ZStack {
VStack {
HStack {
Text("Search OMDB")
.font(.system(size: 25, weight: .black, design: .rounded))
Spacer()
}
.padding()
Spacer()
SearchBar(text: $viewModel.searchTerm,
onSearchButtonClicked: viewModel.onSearchTapped)
List(viewModel.movieObj.movies) { movie in
Text(verbatim: movie.title)
}
.onAppear() {
print("Got the new data")
}
}
}
}
}
struct SearchBar: UIViewRepresentable {
@Binding var text: String
var onSearchButtonClicked: (() -> Void)? = nil
class Coordinator: NSObject, UISearchBarDelegate {
let control: SearchBar
init(_ control: SearchBar) {
self.control = control
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
control.text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
control.onSearchButtonClicked?()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
When I run the code, the REST api is returning the data, but I am not able to see the same in Movie
array and List
is not displaying anything.
EDIT: Adding the sample json returned by the REST API
{
"Search": [
{
"Title": "What We Do in the Shadows",
"Year": "2014",
"imdbID": "tt3416742",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BMjAwNDA5NzEwM15BMl5BanBnXkFtZTgwMTA1MDUyNDE@._V1_SX300.jpg"
},
{
"Title": "I Know What You Did Last Summer",
"Year": "1997",
"imdbID": "tt0119345",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZDI4ODJlNGUtNjFiMy00ODgzLWIzYjgtMzgyZTljZDQ2NGZiXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg"
}
],
"totalResults": "4365",
"Response": "True"
}