Building a gRPC Client in iOS / Swift
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.proto
that 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:
- An Apple computer
- Xcode and Command-Line-Tools, Apple’s IDE and compiler tools for building iOS projects (requires an Apple computer)
- GoLang, a programming language made by Google
- Homebrew, a Terminal-based MacOS software manager (requires Xcode)
- Cocoapods, a package manager for Xcode project libraries and frameworks (requires Homebrew)
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 NIOclass 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 CoreDatastruct 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.