I'm trying to build an app that searches GitHub repositories. And I'm using MVVM and Moya. When I call a method to search I get table view reloaded before a server responds.
So the app flow should be:
A user types in a search query into the search bar. In searchBarSearchButtonClicked(_:)
of SearchViewController
called my method to search. The method is inside SearchViewModel
. So view model's method triggers RepositoryService
and then NetworkService
.
I put some print statements to see an order of execution. And in the console, I got: 2 3 4 (Here the table view is refreshing) 1
. I've tried to use GCD in different places, also I tried to use barriers. At the end of the day, the table view is still refreshing before print(1)
is called.
SearchViewController:
extension SearchViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let query = searchBar.text, query.count > 2 else { return }
viewModel.searchRepositories(withQuery: query) { [weak self] in
guard let self = self else { return }
print(4)
print("Reloading...")
self.tableView.reloadData()
}
}
}
SearchViewModel:
var repositories = [Repository]()
func searchRepositories(withQuery query: String, completion: @escaping () -> Void) {
repositories = repositoryService.searchRepositories(withQuery: query)
print(3)
completion()
}
RepositoryService:
private var repositories = [Repository]()
func searchRepositories(withQuery query: String) -> [Repository] {
networkService?.searchRepositories(withQuery: query) { [weak self] repositories in
guard let self = self, let repositories = repositories else { return }
self.repositories += repositories
}
print(2)
return self.repositories
}
NetworkService:
func searchRepositories(withQuery query: String,
completionHandler: @escaping (([Repository]?) -> Void)) {
provider?.request(.searchRepo(query: query)) { result in
switch result {
case .success(let response):
do {
let repositories = try response.map(SearchResults<Repository>.self)
print(1)
completionHandler(repositories.items)
} catch let error {
print(error.localizedDescription)
}
case .failure(let error):
print(error)
}
}
}
Your issue is basically that you are not handling async requests properly.
Consider the code:
var repositories = [Repository]()
func searchRepositories(withQuery query: String, completion: @escaping () -> Void) {
repositories = repositoryService.searchRepositories(withQuery: query) // here we are going to create a network request in the background, which takes time.
print(3)
completion() // when you call this, your network request is still trying, so when your tableView refreshes... the data hasn't returned yet.
}
As you can see in the comments, what will happen here is:
What you should be doing...
func searchRepositories(withQuery query: String, completion: @escaping (SomeResponseType?, Error?) -> Void) {
repositories = repositoryService.searchRepositories(withQuery: query) { response in
completion(results, error)
}
}
You have followed this pattern elsewhere but you need to cover all async requests.
Same here:
func searchRepositories(withQuery query: String) -> [Repository] {
networkService?.searchRepositories(withQuery: query) { [weak self] repositories in
guard let self = self, let repositories = repositories else { return }
self.repositories += repositories
}
print(2)
return self.repositories
}
return self.repositories
will be called instantly after the network call starts and the network request would not of had time to return the data.
Couple of useful resources on the topic: