Preparing iOS client for GraphQL

  • Tutorial
image

I am sure that each of us at least once experienced problems with the REST API. Eternal battles with backing for the desired API format, several screen requests, and more. Agree that this is not uncommon, but a daily routine. And recently, Tribuna Digital launched a new project - Betting Insider. Initially, the project was implemented on iOS and Android, and later development of the web version began. The existing API was very inconvenient for the web. All this led to the fact that we decided to arrange an experiment and try GraphQL together with a client from Apollo. If you want to get acquainted with this technology in iOS closer, then welcome to cat!

A Little About GraphQL


GraphQL - (Graph Query Language) is a query language and instance for processing these queries. Simply put, this is a layer between our servers and the client, which gives the client what it needs from the server. Communication with this layer occurs directly in GraphQL. If you want to read more directly about GraphQL Language and more, or don’t know anything at all, then read here and here . Everything is detailed enough with pictures.

What do we need?


For further work, we will need NodeJS , Node Package Manager , CocoaPods .
If you want graphql query highlighting, then install this plugin . You also need to download a pre-prepared project . And the most important is getting your personal key. Here then the instruction and ask for here is such a Skopje

  • user
  • repo


What do we do?


We will write a small client for github in which we can find the top 20 repositories for any keyword. To experience the Github API and GraphQL, you can play around here .

Project setup


Before you start writing code, we will install some packages and configure the project.
The Apollo library uses code that generates their own apollo-codegen tool. Install it using npm:

npm install -g apollo-codegen

Next, go to the project folder and open Source. Download the schema of your GraphQL server, and instead insert your personal key, which you should have done above:

apollo-codegen download-schema https://api.github.com/graphql --output schema.json --header "Authorization: Bearer "

Create a GraphQL folder in the Source folder.

Next, you need to register a script for our code generator:

APOLLO_FRAMEWORK_PATH="$(eval find $FRAMEWORK_SEARCH_PATHS -name "Apollo.framework" -maxdepth 1)"
if [ -z "$APOLLO_FRAMEWORK_PATH" ]; then
echo "error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project."
exit 1
fi
cd "${SRCROOT}/Source"
$APOLLO_FRAMEWORK_PATH/check-and-run-apollo-codegen.sh generate $(find . -name '*.graphql') --schema schema.json --output GraphQL/API.swift





At the first start of the project assembly, this script will create an API.swift file that will contain the necessary code to work with your requests and will be updated with each assembly. At the moment, Xcode will throw an error at the build stage, as we are missing something.

Inquiries


To create an API, a code generator needs a file where all requests in GraphQL will be written. Create a new empty file in the Source / GraphQL folder (at the very bottom of the list of files) with the name github.graphql. Next, let's write query in this file, which will give us the first 20 repositories for the search query, and also immediately determine the fragment for the repository.

query SearchRepos($searchText: String!) {
    search(first: 20, query: $searchText, type: REPOSITORY) {
        nodes {
            ... on Repository {
                ...RepositoryDetail
            }
        }
    }
}
fragment RepositoryDetail on Repository {
    id
    nameWithOwner
    viewerHasStarred
    stargazers {
        totalCount
    }
}

Next, build the project so that the code we need is generated. Add the resulting API.swift file to the project.

IMPORTANT: If you added new code to * .graphql, then first build the project and only then start writing new code!

Client


You must create an Apollo client. Create it in AppDelegate.swift above the AppDelegate declaration. Since communicating with github requires authorization, you need to add header to all our requests. We get such a thing:

let apollo: ApolloClient = {
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = ["Authorization": "bearer "]
    let url = URL(string: "https://api.github.com/graphql")!
    return ApolloClient(networkTransport: HTTPNetworkTransport(url: url, configuration: configuration))
}()

Search query


Now you need to make sure that when you click on the Search button, we send a request to the server. Create a ReposListViewModel.swift file, and a ReposListViewModel file. This will be a class that will send our requests to the server, as well as manage the status of existing requests.

First, import Apollo, and then create a variable of type Cancellable and call it currentSearchCancellable:

import Apollo
class ReposListViewModel {
    private var currentSearchCancellable: Cancellable?
}

Next, we create a function that will take the text to search, and we will pass the resulting array to callback. Finally, we proceed directly to sending the request! First you need to create a query. Our code generator generated a query for us, which corresponds to the name that we gave it in the graphql file: SearchReposQuery, and we will initialize it using the search text. Next, to get an answer, we call the fetch function of the Apollo client, into which we pass our query, and also select the main execution queue and the corresponding caching policy. In callback, fetch returns our optional result and error. For now, we won’t think about how to handle the error, but simply print it if it occurs. Pull RepositoryDetail from the result, who also generated a code generator for us and pass them to the callback search function. This function will turn out
:
func search(for text: String, completion: @escaping ([RepositoryDetail]) -> Void) {
        currentSearchCancellable?.cancel()
        let query = SearchReposQuery(searchText: text)
        currentSearchCancellable = apollo.fetch(query: query, cachePolicy: .returnCacheDataAndFetch, queue: .main, resultHandler: { (result, error) in
            if let result = result, let data = result.data {
                let repositoryDetails = (data.search.nodes ?? [SearchReposQuery.Data.Search.Node?]()).map{$0?.asRepository}.filter{$0 != nil}.map{($0?.fragments.repositoryDetail)!}
                completion(repositoryDetails)
            } else {
                print(error as Any)
                completion([RepositoryDetail]())
            }
        })
    }

Now create a viewModel as the parameter ReposListViewController, as well as an array for storing RepositoryDetail:

let viewModel = ReposListViewModel()
var repositoryDetails = [RepositoryDetail]()

Next, change numberOfRowsInSection to:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return repositoryDetails.count
    }

It is also necessary to update ReposListCell:

class ReposListCell: UITableViewCell {
    @IBOutlet weak var repoName: UILabel!
    @IBOutlet weak var starCount: UILabel!
    @IBOutlet weak var starButton: UIButton!
    var repositoryDetail: RepositoryDetail! {
        didSet {
            repoName.text = repositoryDetail.nameWithOwner
            starCount.text = "\(repositoryDetail.stargazers.totalCount)"
            if repositoryDetail.viewerHasStarred {
                starButton.setImage( imageLiteral(resourceName: "ic_full_star"), for: .normal)
            } else {
                starButton.setImage( imageLiteral(resourceName: "ic_empty_star"), for: .normal)
            }
        }
    }
}

And cellForRow will take this form:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ReposListCell") as! ReposListCell
        cell.repositoryDetail = repositoryDetails[indexPath.row]
        return cell
    }

It remains to request viewModel the necessary data for us to click:

 func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        viewModel.search(for: searchBar.text ?? "") { [unowned self] repositoryDetails in
            self.repositoryDetails = repositoryDetails
            self.tableView.reloadData()
        }
    }

Done! Compile and search the top 20 repositories for your request!

Put the stars


As you already understood, the star in the layout of the cell is not just. Let's add this functionality to our application. To do this, create a new mutation request, but before that, let's get your ID here . Now you can insert a search query with the appropriate clientMutationID and build the project:

mutation AddStar( $repositoryId: ID!) {
    addStar(input: {clientMutationId: “”, starrableId: $repositoryId}) {
        starrable {
            ... on Repository {
               ...RepositoryDetail
            }
        }
    }
}
mutation RemoveStar($repositoryId: ID!) {
    removeStar(input: {clientMutationId: “”, starrableId: $repositoryId}) {
        starrable {
            ... on Repository {
                ...RepositoryDetail
            }
        }
    }
}

Add the execution of these requests to the ViewModel. The logic is the same as with query, but instead of fetch, we now call perform:

func addStar(for repositoryID: String, completion: @escaping (RepositoryDetail?) -> Void ) {
        currentAddStarCancellable?.cancel()
        let mutation = AddStarMutation(repositoryId: repositoryID)
        currentAddStarCancellable = apollo.perform(mutation: mutation, queue: .main, resultHandler: { (result, error) in
            if let result = result, let data = result.data {
                let repositoryDetails = data.addStar?.starrable.asRepository?.fragments.repositoryDetail
                completion(repositoryDetails)
            } else {
                print(error as Any)
                completion(nil)
            }
        })
    }
    func removeStar(for repositoryID: String, completion: @escaping (RepositoryDetail?) -> Void ) {
        currentRemoveStarCancellable?.cancel()
        let mutation = RemoveStarMutation(repositoryId: repositoryID)
        currentAddStarCancellable = apollo.perform(mutation: mutation, queue: .main, resultHandler: { (result, error) in
            if let result = result, let data = result.data {
                let repositoryDetails = data.removeStar?.starrable.asRepository?.fragments.repositoryDetail
                completion(repositoryDetails)
            } else {
                print(error as Any)
                completion(nil)
            }
        })
    }

Now you need to let ViewController know that we clicked on the button. Let's make it a cell delegate. To do this, create a protocol and add it to the ReposListCell.swift file:

protocol ReposListCellDelegate: class {
    func starTapped(for cell: ReposListCell)
}

Add a new cell class parameter and processing for clicking on the star:
weak var delegate: ReposListCellDelegate?
override func awakeFromNib() {
        super.awakeFromNib()
        starButton.addTarget(self, action: #selector(starTapped), for: .touchUpInside)
    }
    @objc func starTapped() {
        delegate?.starTapped(for: self)
    }

Now assign the controller as a delegate in cellForRow:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ReposListCell") as! ReposListCell
        cell.repositoryDetail = repositoryDetails[indexPath.row]
        cell.delegate = self
        return cell
    }

Add a function that updates the data in the table:
func updateTableView(for newDetail: RepositoryDetail?) {
        if let repositoryDetail = newDetail {
            for (index, detail) in repositoryDetails.enumerated() {
                if detail.id == repositoryDetail.id {
                    self.repositoryDetails[index] = repositoryDetail
                    for visibleCell in tableView.visibleCells {
                        if (visibleCell as! ReposListCell).repositoryDetail.id == repositoryDetail.id {
                            (visibleCell as! ReposListCell).repositoryDetail = repositoryDetail
                        }
                    }
                }
            }
        }
    }

And it remains to make the controller extension for the previously created controller:

extension ReposListViewController: ReposListCellDelegate {
    func starTapped(for cell: ReposListCell) {
        if cell.repositoryDetail.viewerHasStarred {
            viewModel.removeStar(for: cell.repositoryDetail.id) { [unowned self] repositoryDetail in
                self.updateTableView(for: repositoryDetail)
            }
        } else {
            viewModel.addStar(for: cell.repositoryDetail.id) { [unowned self] repositoryDetail in
                self.updateTableView(for: repositoryDetail)
            }
        }
    }
}

Done! We can launch the application, search for the top 20 repositories and put / remove stars!

That is not all. It is interesting to use this library in conjunction with RxSwift, solve the pagination problem, and Apollo also supports caching, but about everything next time!

If you want to solve such problems with us, join us! Questions and resumes can be sent to jobs@tribuna.digital.

More jobs here!

Also popular now: