Building a gRPC Client in iOS / Swift

Adonis Gaitatzis
8 min readOct 24, 2021

In this article we will learn how to build a simple gRPC client in iOS / Swift 5.

What is gRPC

gRPC is a way for clients and servers to communicate with each other over the Internet. It is intended as an upgrade from REST APIs, with some great features:

  • It transmits faster by using a binary format instead of JSON.
  • It uses a language-agnostic data format, which can be defined and passed. around to clients without the need for per-language developer documentation.
  • It translates its common data format into the native data formats of the server and client.
  • It supports versioning, so future versions of server code don’t require a total rewrite of the client.
  • It defines how to find the endpoints, so URL naming is no longer an issue.
  • It works over HTTP/3, which enables bidirectional streaming with less network latency.

The down sides (at the time of writing) are:

  • It requires custom compiler code for each language to perform all the conversions and binary format conversions.
  • The mechanics of operation are totally obscure to developers. It is not human-readable the way JSON and REST are.
  • The toolchains are new and not very well supported. Sometimes getting a simple call and response to work can take hours or days to get working.
  • It isn’t yet well supported or well documented on all platforms.
  • HTTP/3 is poorly supported by browsers and app software, especially over at Apple.

In this tutorial, I will attempt to walk you through the process I went through to get a simple gRPC client working in React Native on iOS.

How gRPC Works

Basically how gRPC works is that a developer creates a .proto file, which describes objects and methods which will be provided by a service. This file acts as a sort of developer documentation and a reference for a protobuffer compiler which converts the .proto definitions into the local language’s data types.

The protobuffer compiler creates a library that the developer can interact with, abstracting the messaging layer of the API.

This allows a developer to write API logic without having to manage API versions, HTTP endpoints, or JSON parsing.

Demo Server And Protobuffer Definition

For this tutorial we will create a simple Protobuffer definition called authService.protothat provides two methods, Login() and Logout().

syntax = "proto3";
package authService;
option objc_class_prefix = "RTG"; // important for iOS / Xcode
// Define a service
// Define data types here
message AccountCredentials {
string username = 1;
string password = 2;
}
message OauthCredentials {
string token = 3;
uint32 timeoutSeconds = 4;
}
// Define the service containing methods here
service AuthServiceRoutes {
// Basic function call, makes request and returns value
rpc Login(AccountCredentials) returns (OauthCredentials) {}
rpc Logout(OauthCredentials) returns (OauthCredentials) {}
}

This authService.proto file will be available to both the client and server at the time of development, so that the protobuffer compiler can create RPC endpoints.

We can see how to build a RPC server in NodeJS from this protobuffer file, which we can use as the server portion of this tutorial.

Requirements

In order to build this project, we will need some development tools:

This tutorial was written using Swift version 5.4.2 on Xcode 12.5.1.

gRPC functionality requires iOS 14 or newer.

Installing Toolchain

Assuming you already have the required tools listed above set up properly, you can proceed with installing the toolchain, which consists of:

  • protobuf, a tool that compiles gRPC .proto definition files into native libraries.
  • grpc-swift, a plugin for protobuf that lets you compile .proto files into a Swift library

Install the Protocol Buffer Compiler, which compiles .proto files into a native code library in one of several languages. Open a terminal and install protobuf using Homebrew:

$ brew install protobuf

Then install the Swift plugin that allows us to compile .proto files into Swift-compatible code.

$ mkdir ~/sandbox
$ cd ~/sandbox
$ git clone https://github.com/grpc/grpc-swift
$ cd grpc-swift
$ make plugins
$ cp .build/release/protoc-gen-swift .build/release/protoc-gen-grpc-swift /usr/local/bin

Configuring Project

Install protobuf and grpc-swift

Remember protobuf and grpc-swift? Using the protoc command, you can compile a .proto file into a Swift library.

Place the authService.proto in your Xcode project folder and run this command:

$ cd /path/to/xcode/project
$ protoc authService.proto \
--grpc-swift_opt=Client=true,Server=false \
--grpc-swift_out=ios/
--proto_path=. \
--swift_opt=Visibility=Public \
--swift_out=ios/

That generates two new files:

  • authService.grpc.swift, which contains the implementation of your generated service classes
  • authService.pb.swift, which contains the implementation of your generated message classes

The basic format for using this command is:

$ protoc proto_file.proto \
--proto_path=/path/to/proto/file \
--grpc-swift_opt=Client=<bool>,Server=<bool> \
—-grpc-swift_out=/path/to/xcode_project_dir/
--swift_opt=Visibility=<bool> \
--swift_out=/path/to/xcode_project_dir/

Set up Cocoapods

Install Cocoapods in your project by running this command in your terminal from the project folder:

$ pod init

Doing this will generate a file called Podfile which is used to configure a framework installation manager.

Install Frameworks

Next we need to update the project’s Podfile, which Cocoapods will use to install required frameworks that allow us to interact with gPRC in our code.

Update your Podfile to include the following pod definitions inside the target definition:

platform :ios, '14.0' // Minimum iOS version for gRPC is 14.
target ‘ios-only-grpc-login-logout-client’ do
use_frameworks!
pod ‘gRPC-Swift’, ‘~> 1.5.0’ # Latest at the time of writing
pod ‘gRPC-Swift-Plugins’
end

From your Terminal, install the new frameworks with this command:

$ cd <xcode-project-dir>
$ pod install

This should create a <xcode-project-name>.xcworkspace folder, which you can use to open Xcode.

Open the working folder in Finder and then double click this <xcode-project-name>.xcworkspace to open Xcode.

You’ll be using the .xcworkspace file to open this project from now on.

$ open . & # open Finder
$ open ios-only-grpc-login-logout-client.xcworkspace & # open Xcode project

When Xcode opens, drag the two new files (authService.grpc.swift and authService.pb.swift) into the Xcode project under <xcode-project-name/xcode-project-name> and select “Copy Items if Needed” and “Create folder references” to make sure they are included in the compile later.

You’ll be dragging it into the same folder as the Info.plist.

Now is a good time to build the project to make sure there are no errors.

Building Features

Now let’s create the functionality. Create a new Swift File, name it AuthClient.swift

import Foundation
import GRPC
import NIO
class AuthClient {
var authServiceClient: AuthService_AuthServiceRoutesClient?
let port: Int = 50051
init() {
// build a fountain of EventLoops
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
do {
// open a channel to the gPRC server
let channel = try GRPCChannelPool.with(
target: .host(“localhost”, port: self.port),
transportSecurity: .plaintext,
eventLoopGroup: eventLoopGroup
)
// create a Client
self.authServiceClient = AuthService_AuthServiceRoutesClient(channel: channel)
print(“grpc connection initialized”)
} catch {
print(“Couldn’t connect to gRPC server”)
}
}
/**
* Unary call example. Calls `login` and prints the response.
* This method calls the authService.AuthServiceRoutes.login()
* method and tries to understand the response.
*
* It sends a `AuthService_AccountCredentials` object, found in
* authervice.pb.swift through the gRPC method and receives a
* `AuthService_OauthCredentials` object back,
* which it attempts to parse.
*/
func login(username: String, password: String) -> String {
print(“Login: username=\(username)”)
// build the AccountCredentials object
let accountCredentials: AuthService_AccountCredentials = .with {
$0.username = username
$0.password = password
}
// grab the login() method from the gRPC client
let call = self.authServiceClient!.login(accountCredentials)
// prepare an empty response object
let oauthCredentials: AuthService_OauthCredentials
// execute the gRPC call and grab the result
do {
oauthCredentials = try call.response.wait()
} catch {
print(“RPC method ‘login’ failed: \(error)”)
// it would be better to throw an error here, but
// let’s keep this simple for demo purposes
return “”
}
// Do something interesting with the result
let oauthToken = oauthCredentials.token
print(“Logged in with oauth token ‘\(oauthToken)’”)
// return a value so we can use it in the app
return oauthToken
}
func logout(oauthToken: String) {
print(“Logout: token=\(oauthToken)”)
// build the OauthCredentials object
let oauthCredentials: AuthService_OauthCredentials = .with {
$0.token = oauthToken
}
// grab the logout() method from the gRPC client
let call = self.authServiceClient!.logout(oauthCredentials)
// execute the gRPC call and grab the result
do {
_ = try call.response.wait()
print(“Logged out”)
} catch {
print(“RPC method ‘logout’ failed: \(error)”)
// it would be better to throw an error here, but
// let’s keep this simple for demo purposes
return
}
}
}

Try to compile again. Hopefully no crashing.

That works, now we need to build the UI.

Building Views

Now we can add two text fields and two buttons plus a text view inside a vertical stack:

  • TextField 1: username, connected to @State var username
  • TextField 2: password, connected to @State var password
  • Button 1: Login, which executes dologin()
  • Button 2: Logout, which executes doLogout()
  • TextView to display the OAuth token, bound to @State var oauthToken

The basic UI will look like this:

The doLogin() button will call the GrpcAuthClient.login() method and print out the resulting token. The doLogout() method will call the AuthClient.logout() method and clear oauthToken :

import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
// placeholders for TextFields
public let usernamePlaceholder: String = "Username"
public let passwordPlaceholder: String = "Password"
// data stores for TextFields (with default values set)
@State var username = "email@example.com"
@State var password = "password"
// initialize our gRPC AuthService client
let grpcAuthClient = AuthClient()
// data store for our login Auth token
@State var oauthToken = "(none)"
@State var isLoggedIn = false
var body: some View {
VStack {
TextField(usernamePlaceholder, text: $username)
TextField(passwordPlaceholder, text: $password)
Button(action: {
doLogin()
}) {
Text("Login")
}
Button(action: {
doLogout()
}) {
Text("Logout")
}
Text("Oauth Token: \(oauthToken)")
}
}
private func doLogin() {
// execute the login() method in our gRPC client
self.oauthToken = self.grpcAuthClient.login(
username: self.username,
password: self.password
)
self.isLoggedIn = self.oauthToken.count > 0
}
private func doLogout() {
// execute the logout() method in our gRPC client
self.grpcAuthClient.logout(oauthToken: self.oauthToken)
self.isLoggedIn = false
self.oauthToken = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

If you don’t already have the gRPC server running, now is a good time to start. Open a terminal and run:

$ cd ~/sandbox/nodejs-grpc-auth-service
$ node server.js

Compile and run your iOS project. You should see a simple UI that lets you click a “login” button to grab an OAuth token from the gRPC server.

If you run the gRPC server in a Terminal and click the “Login” button in the iOS simulator, authService.login() RPC call will be executed on the server and an OAuth token will be sent back to the iOS client.

And congratulations, you have a gRPC client in iOS.

--

--

Adonis Gaitatzis
Adonis Gaitatzis

Written by Adonis Gaitatzis

Is a technology nerd who can synthesize any business need into a technology solution. Fun projects have included brain imaging tech, ML, and blockchain.

Responses (1)