Skip to main content
added 7 characters in body
Source Link
Martin R
  • 23.5k
  • 2
  • 36
  • 92
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"
    }
}
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"
    }
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"
    }
}
Source Link
Martin R
  • 23.5k
  • 2
  • 36
  • 92

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.