I cannot review all aspects of your code, but here are some remarks which might be helpful.
JSON decoding
In the definition of User.CodingKeys
one can omit the raw values of keys which are equal to the case name:
struct User: Decodable {
let login: String
let id: Int
let avatarUrl: String
let url: String
let reposUrl: String
enum CodingKeys: String, CodingKey {
case login // Instead of: case login = "login" ...
case id
case avatarUrl = "avatar_url"
case url
case reposUrl = "repos_url"
}
And the conversion from “snake-case” (in the JSON API) to “camel-case” (in the Swift API) can be done by setting a keyDecodingStrategy
on the JSONDecoder
in getUsers()
:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
AF.request(url, parameters: nil).validate()
.responseDecodable(of: SearchResponse.self, decoder: decoder) { response in
// ...
}
which makes the User.CodingKeys
obsolete:
struct User: Decodable {
let login: String
let id: Int
let avatarUrl: String
let url: String
let reposUrl: String
}
Unnecessary conversions to/from Any
The functions in ServiceManager
all take a callback argument of type (_ response:Any?) -> Void
. The (correctly typed) response from AlamoFile is therefore cast to Any?
, and then cast back to the correct type in the caller of that function.
It becomes simpler with a callback that takes the proper type, e.g. in getUsers()
:
public static func getUsers(for query: String, completion: @escaping (SearchResponse) -> Void) {
// ...
AF.request(url, parameters: nil).validate()
.responseDecodable(of: SearchResponse.self, decoder: decoder) { response in
guard let result = response.value else { ... }
completion(result)
}
}
which allows to get rid of the conditional case in the calling function searchUsers()
:
ServiceManager.getUsers(for: name) {[weak self] (response) in
guard let self = self else { return }
// guard let response = response as? SearchResponse else { return }
// ^-- No longer needed.
self.users = response.users
print("RESPONSE", name, response.users.count)
}
Handle rate limits and paginated responses
Of course the first thing I did was to download your code and compile the app. That worked flawlessly, it compiled without warnings. Great!
Then I ran the app in the iOS Simulator. At first it seemed to work correctly, but then I typed my GitHub user name into the search field: No result – what is that? OK, delete the search field and try again: Now the tableview wasn't updated at all. Nothing seemed to happen anymore.
To isolate the problem, I added a
print("ERROR", response.error)
in the failure part of getUsers()
. This revealed that after some time, the GitHub API responded with an HTTP error 403. And indeed, the GitHub Search API documentation states that search requests are limited to 30 requests per minute, and even to 10 requests per minute for unauthenticated users.
There is not much one can do about this limit, but a small remedy might be a “delayed search”: If the users type "ABCD" then there is no need to search for "A", then for "AB", "ABC", and finally for "ABCD". If you start a short timer after each modification to the search text, and start the actual search only if the search text is not modified for some time then the number of search requests is already reduces.
You could also cache search results in order to submit fewer requests.
Also GitHub responses are paginated to 30 items by default. This can be increased to 100 at most, but even then you'll normally not get all items with a single request.
The link header in the search response contains the necessary information. You can use that to either
- issue more request for the remaining items. Perhaps only when the user actually scrolls to the end of the current list, again in order to limit the number of requests.
- or just display “More ...” at the end of the list, so that the user knows that there are more items than are currently displayed.