Create an API client library for iOS in Swift

Eric Vargas
Eric Vargas
Blog Image
Client libraries are shared code to avoid repetitive tasks. Engineers love client libraries. In iOS, libraries are also referred to as frameworks or SDKs. In this post, I’ll stick to using the term library.
I’ll show you a common pattern to build a library. Libraries are used everywhere.
If you think this is a daunting task, worry not! Keep reading and you’ll see it’s easier than you think.

The Library

The goal of this post will be to build a library that
  • Retrieves the latest price of a list of cryptocurrencies
  • The library is used in a sample app that shows these prices
  • You are ready to create your own library

The Sample Server API

In this example, I will use the CoinMarketCap API. With this API you can retrieve current and historic information about cryptocurrencies.
By checking their documentation, you will notice their API is extensive. For this post, only the v2/cryptocurrency/quotes/latest/ endpoint will be used.
Check out their Quick Started Guide to create your own API KEY. You will need one to make requests to their service.

Creating the Swift Package

Nowadays it is very simple to create a new library in Swift. In iOS, the modern way to publish and deliver libraries is via the Swift Package Manager. From here on out, I’ll walk you step-by-step on how to create the library.
My requirements are:
  • Xcode 13.4.1
  • The library will support iOS 10+
Create a new Xcode project. Make sure you select Swift Package as the project type.
notion image
Follow the instructions and you’ll decide where to save the project on your machine. I named the library MyCryptoCoinSwiftLibrary for this tutorial. After the project is created, notice the file named Package.swift.
// swift-tools-version: 5.6 import PackageDescription let package = Package( name: "MyCryptoCoinSwiftLibrary", products: [ // Products define the executables and libraries // a package produces, and make them visible to // other packages. .library( name: "MyCryptoCoinSwiftLibrary", targets: ["MyCryptoCoinSwiftLibrary"]), ], dependencies: [ // Dependencies declare other packages that this package // depends on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. // A target can define a module or a test suite. // Targets can depend on other targets in this package, // and on products in packages this package depends on. .target( name: "MyCryptoCoinSwiftLibrary", dependencies: []), .testTarget( name: "MyCryptoCoinSwiftLibrary Tests", dependencies: ["MyCryptoCoinSwiftLibrary"]), ] )
If you want to know more about each field in this file, take a gander at the Package Description site by Apple. Know that in a package, you can add resources like images and videos, and not just code. The library in this post is fairly simple because it only interacts with a server API.
I am removing the .testTarget from the list of targets. I will not focus on how to write unit tests for our library in this tutorial. Yet, unit tests are very important for a real library, you want make sure you doesn’t introduce critical bugs.
Because the library needs to connect with the CoinMarketCap API, a networking layer is needed. For that, our library will make use of the most famous networking library in iOS, Alamofire. In their documentation, there are instructions on how to add it as a dependency in our Package.swift, check out Almofire’s instructions here.
This sample library will only support iOS, so make sure to list it in the platforms field.
After those modifications, Package.swift now looks like this:
// swift-tools-version: 5.6 import PackageDescription let package = Package( name: "MyCryptoCoinSwiftLibrary", platforms: [ .iOS(.v10) ], products: [ // Products define the executables and libraries a // package produces, and make them visible to // other packages. .library( name: "MyCryptoCoinSwiftLibrary", targets: ["MyCryptoCoinSwiftLibrary"]), ], dependencies: [ // Dependencies declare other packages that this package // depends on. // .package(url: /* package url */, from: "1.0.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.1")) ], targets: [ // Targets are the basic building blocks of a package. // A target can define a module or a test suite. // Targets can depend on other targets in this package, // and on products in packages this package depends on. .target( name: "MyCryptoCoinSwiftLibrary", dependencies: ["Alamofire"], // Important so // Alamofire can be used in your library. path: "Sources"), // Short explanation: What // you want the library to expose to the public. ] )
After you add dependencies to your library, you should see them listed in your Xcode project. From here, the setup is done and all that is left is the code!
The code will be simple. I won’t overcomplicate the library with a complex design. There will be a struct called CoinRetriever that will expose only one function
public func latestPrice( coins: [String], completionHandler: @escaping (Result< Coins, AFError>) -> Void)
This expects to receive a list of coins to retrieve their latest price. For this tutorial keep it simple and stick with just supporting the cryptocurrency symbol, for example, “BTC”, “ETH”.
It will return the response with the block completionHandler. Notice that Coins is listed as the success value, and this is something that I’ll explain now.

Models

This function will call the CoinMarketCap API endpoint /v2/cryptocurrency/quotes/latest. As listed in their docs, a sample response is:
{ "data": { "BTC": { "id": 1, "name": "Bitcoin", "symbol": "BTC", "slug": "bitcoin", "is_active": 1, "is_fiat": 0, "circulating_supply": 17199862, "total_supply": 17199862, "max_supply": 21000000, "date_added": "2013-04-28T00:00:00.000Z", "num_market_pairs": 331, "cmc_rank": 1, "last_updated": "2018-08-09T21:56:28.000Z", "tags": [ "mineable" ], "platform": null, "self_reported_circulating_supply": null, "self_reported_market_cap": null, "quote": { "USD": { "price": 6602.60701122, "volume_24h": 4314444687.5194, "volume_change_24h": -0.152774, "percent_change_1h": 0.988615, "percent_change_24h": 4.37185, "percent_change_7d": -12.1352, "percent_change_30d": -12.1352, "market_cap": 852164659250.2758, "market_cap_dominance": 51, "fully_diluted_market_cap": 952835089431.14, "last_updated": "2018-08-09T21:56:28.000Z" } } } }, "status": { "timestamp": "2022-06-02T14:44:22.210Z", "error_code": 0, "error_message": "", "elapsed": 10, "credit_count": 1 } }
Instead of parsing the JSON, a better approach is to create models that represent the response of the API.
From bottom to top, three models can be identified for this response.
Quote
public struct Quote: Decodable { public let price: Double }
Coin
public struct Coin: Decodable, Identifiable { public let id: Int public let name: String public let symbol: String public let quote: [String: Quote] }
The only reason Coin extends Identifiable is because this model is used in the sample app. This model is the input of a List View. To simplify the classes that are used in this tutorial, the List View requires the model to be Identifiable.
In a real scenario, I would suggest creating an extra model or class that is used in the UI, to keep this one decoupled from the client logic.
Coins
public struct Coins: Decodable { public let data: [String: [Coin]] }
Notice that all the members of these models match a value from the API response. As an example, let’s say that Coin also cares about max_supply from the response, in that case Coin would look like
public struct Coin: Decodable, Identifiable { public let id: Int public let name: String public let symbol: String public let maxSupply: Int public let quote: [String: Quote] enum CodingKeys: String, CodingKey { case id case name case symbol case maxSupply = "max_supply" case quote } }
This is to illustrate what is needed to support names that do not map between the response and the model members. No need to add it for this tutorial.

Requests

Now create the CoinRetriever struct and add the following code
import Alamofire // 1. Import dependency listed in Package.swift public struct CoinRetriever { private var apiKey: String; // 2. apiKey is needed for the // CoinMarketCap API public init(apiKey: String) { self.apiKey = apiKey; } public func latestPrice(coins: [String], completionHandler: @escaping (Result< Coins, AFError>) -> Void) { let headers: HTTPHeaders = [ // 3. Headers needed by // CoinMarketCap "X-CMC_PRO_API_KEY": apiKey, "Accept": "application/json", "Accept-Encoding": "deflate, gzip" ] let parameters: Parameters = ["symbol": coins.joined(separator: ",")] // 4. The parameter that this method will support. // E.g. "BTC", "ETH" // 5. Alamofire request to the endpoint, it decodes // the value to the Coins model previously created let endpoint = "https://sandbox-api.coinmarketcap.com" + "/v2/cryptocurrency/quotes/latest" AF.request(endpoint, parameters: parameters, headers: headers) .responseDecodable(of: Coins.self) { response in guard let coins = response.value else { completionHandler(.failure(response.error!)) return } completionHandler(.success(coins)) } } }
Steps to make a request using Alamofire and decoding the response into the models.
  1. Import the dependency listed in Package.swift. This will be used to make the actual request to the API.
  1. CoinMarketCap needs an API KEY to authenticate its request, if you still don’t have one, read here how to get one.
  1. Apart from putting the API KEY in the headers, they also suggest adding extra headers.
  1. Check the endpoint docs to see what other parameters are supported. For this tutorial only “symbol” will be used.
  1. Alamofire does the request. It decodes the response as the Coins model created before.

Publish the library

This step is the simplest of all. SPM works with Github out of the box. All you need to do is upload the library project to a Github repository. In this case, I created the repo github/MyCryptoCoinSwiftLibrary. From here, it’s all about advertising your repo, which is out of the scope of this post.

Extra Step - A Sample App

As an extra step, you might be wondering how would someone use your new library. To demonstrate this, create a sample app.
In Xcode create a new iOS app. I named mine CryptoCoinSampleApp.
As a suggestion, add the sample app to the same directory where you created the library. This way you will include a sample app alongside your library as part of your repository.
Now the important step is how to add your library that now lives in github. As a matter of fact, this process is the same for any other library published through SPM.
In your Xcode app project, click on File → Add Packages (remember the Xcode version I used! this UI/menu can change in a different Xcode version). It will open up a menu like the following
notion image
Notice I searched for the Github repo where the library was published, that’s how you will find yours too. In a production library, instead of using a specific branch to get the code from, the normal use case is to specify a release version, something like 1.0.0. By the time you are using this approach you should be more comfortable building libraries.
After this, your code will have access to your new library, and you can import it like this:
import MyCryptoCoinSwiftLibrary
Since the app itself is out of the scope of this tutorial, I will only point you to it in the github repository.
Play with it! Create a PR with a suggestion, create an issue in the repo, let me know what you think! Feel free to use the library as well in your own sample app!
notion image
The previous screenshot shows the response of the library in the sample app.

The LibLab way

As you work on this tutorial, you might notice that a lot of what was coded is very repetitive and tedious. At LibLab, our goal is to automate this process. We have a product that will let you create what I explained in this tutorial in an automatic way. For a lot of languages and different platforms, not just Swift and iOS.
Follow our Twitter and join our Discord to connect with us and soon enough join our beta launch! Check the LibLab website for all the news and more of this content!