Building a gRPC Client in React Native on iOS
In this article we will learn how to build a simple gRPC client in React Native by bridging a native module on iOS / Swift 5.
Overview
At the time of writing, gRPC isn’t supported by React Native. iOS supports it though.
React Native was designed to talk to Objective-C in iOS using native modules, but not to Swift. Since Swift can connect classes and methods to Objective-C bindings, it is possible to get React Native to act as a gRPC client by binding React Native to an Objective-C bridge, which talks to your Swift code.
Note that the grpc-web library is not compatible with React Native as of 0.66.
So, to get this working you will need to:
- Install the toolchain
- Create a React Native project
- Install iOS gRPC dependencies
- Compile Protobuffer files
- Program gRPC client in Swift
- Expose gRPC client methods as a native module
- Build React Native native module class for gRPC client
- Integrate with UI
- Build and test
The gRPC server we will be using is authService
from another tutorial that shows how to build a gRPC Server in NodeJS. Our React Native client will be able to log in, retrieve an authentication token, and log out.
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)
- NodeJS, a command-line programming Javascript implementation
- React Native, a universal front-end development framework for mobile applications (requires NodeJS)
This tutorial was written using React Native version 0.66.1, Swift version 5.4.2 on Xcode 12.5.1.
1. Install the 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 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
This will install the protoc
program, which compiles .proto
files.
Next, install the protoc
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
2. Create React Native Project
The first thing you must do is create a new React Native project. This will create a template React Native project which could be deployed on both iOS and Android, but in this tutorial we will only be dealing with the iOS side of things.
$ npx react-native init RnNativeGrpcClient
$ cd RnNativeGrpcClient
Now is a good time to compile and make sure there are no problems. Run this command to deploy onto your iOS Simulator:
$ npx react-native run-ios
# or
$ npm run ios
3. Install iOS gRPC Dependencies
The next step consists of installing the Cocoapod frameworks that allow you to use the gRPC APIs in your Swift code.
$ open . & #open Finder
$ open ios/RnNativeGrpcClient.xcworkspace & # Open Xcode project
Open the Podfile
and make these three changes:
- Update to target version “14”:
platform :ios, ‘14.0’
2. Remove use_flipper!()
and replace with use_frameworks!
3. Add the following lines after use_frameworks!
to tell Cocoapods to install the gRPC-Swift frameworks.
pod ‘gRPC-Swift’, ‘~> 1.5.0’ # Latest at the time of writing
pod ‘gRPC-Swift-Plugins’
With those changes, you can install the new frameworks from your Terminal and watch the magic happen:
$ npx pod-install
4. Compile Protobuffer file
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/
$ protoc authService.proto \
--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
These files describe the service and the related message classes derived from the .proto
files.
For example,
AuthServiceRoutes
in the Protobuffer file will be mapped toAuthService_AuthServiceRoutesClient
in Swift.AccountCredentials
in the Protobuffer file will be mapped toAuthService_AccountCredentials
in SwiftOauthCredentials
in the Protobuffer file will be mapped toAuthService_OauthCredentials
Drag these 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.
$ npx react-native run-ios
5. Expose gRPC Client Methods as Native Module
In Xcode, Create a new Swift File (Cmd+n). For this tutorial, we will name it AuthClient
. Click “Next”
Xcode will ask if you want to create an Objective-C bridging header. Since you are bridging your Swift code to React-Native through Objective-C, that’s exactly what we want. Click “Create Bridging Header”
You’ll end up with two new files:
AuthClient.swift
<project-name>-Bridging-Header.h
To bridge Objective-C to React-Native, you’ll need to import the RCTBridgeModule.h
Add this to <project-name>-Bridging-Header.h
:
#import “React/RCTBridgeModule.h”
Now let’s look at AuthClient.swift
. This will serve as both the gRPC client and as the Objective-C bridge to React Native.
Normally in Swift, the methods would look like this:
class ClassName {
func methodName() {}
}
However, since we need to bridge through Objective-C, we need to add @objc
decorators to our Swift code, like this:
@obj(ClassName) class ClassName: NSObject {
@obj func login() {}
}
Import Frameworks
Start by importing GRPC
and NIO
frameworks:
import Foundation
import GRPC
import NIO
Define Class
The next step is to define the AuthClient
class and connect the AuthService_AuthServiceRoutesClient
(the class that was auto-generated by the protoc
command). We happen to know from the other tutorial that the server is running on localhost:50051
@objc(AuthClient) class AuthClient: NSObject {
var authServiceClient: AuthService_AuthServiceRoutesClient?
let port: Int = 50051
}
We don’t need this to run on the main thread, so we can set requiresMainQueueSetup()
to return false
:
@objc(AuthClient) class AuthClient: NSObject {
// ...
@objc static func requiresMainQueueSetup() -> Bool {
return false
}
}
Let’s create a method that opens a channel to the gRPCC server. This method does not need to be exposed to React Native:
@objc(AuthClient) class AuthClient: NSObject {
// ...
func createGrpcServiceClient(
eventLoopGroup: EventLoopGroup
) throws -> Lnrpc_WalletUnlockerClient {
let secureGrpcChannel = try GRPCChannelPool.with(
target: .host("localhost", port: 51009),
transportSecurity: .plaintext,
eventLoopGroup: eventLoopGroup
)
return AuthService_AuthServiceRoutesClient(
channel: secureGrpcChannel
)
}
}
Don’t get too excited yet, you’ll be calling this method in your Swift code like this:
// create event loop
let eventLoopGroup = PlatformSupport.makeEventLoopGroup(
loopCount: 1
}
// close the eventLoop when the function exits
defer {
try? eventLoopGroup.syncShutdownGracefully()
}
// retrieve the AuthService_AuthServiceRoutesClient
let authServiceRoutesClient = try self.createGrpcServiceClient(
eventLoopGroup: eventLoopGroup
)
Implement gRPC Methods
The gRPC client should implement two methods: login()
and logout()
:
Since talking to a remove server may take a noticeable amount of time, we don’t want to block the UI thread. Therefore we will be implementing these methods using Javascript Promises.
Since Promises aren’t supported natively in Swift, they are implemented as callbacks using the RCTPromiseResolveBlock
and RCTPromiseRejectBlock
objects.
These callback objects must be the last two parameters in a function, and all parameters must be unnamed.
So the process is this:
@obj func methodName(
_ param1: Object,
param2: Object,
resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
// Do stuff and resolve a dictionary
resolve(["key1": "value1", "key2": "value1"])
}
Then you can call this method in JavaScript:
try {
const grpcMessageResponse = nativeModuleName.methodName(
param1,
param2
)
} catch (error) {
console.log(error.message)
}
Back in Swift, You can throw an error using the reject
callback:
let error = NSError(domain: "", code: 200, userInfo: nil)
reject("0", "This is the error.message text", error)
To execute the method, you must grab it from the client object:
let call = serviceClientObject.methodName(param1 param2)
let responseObject = call.response.wait() // throws error on fail
Parameters are usually gRPC methods, which can be built like this:
let messageTypeName: ServiceName_MessageTypeName = .with {
$0.param1 = "value1"
$0.param2 = "value2"
}
Putting that all together, we can write the login()
method:
@objc(AuthClient) class AuthClient: NSObject {
// ...
@objc func login(
_ username: String,
password: String,
resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
let eventLoopGroup = PlatformSupport.makeEventLoopGroup(
loopCount: 1
)
defer {
try? eventLoopGroup.syncShutdownGracefully()
}
let authServiceClient = try self.createGrpcServiceClient(
eventLoopGroup: eventLoopGroup
) // 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 = authServiceClient.login(accountCredentials)
do {
let oauthCredentials = try call.response.wait()
resolve([
"token": oauthCredentials.token,
"timeoutSeconds": oauthCredentials.timeoutSeconds
])
} catch {
let error = NSError(domain: "", code: 200, userInfo: nil)
reject("0", "RPC method ‘login’ failed", error)
}
}
try? authServiceClient.channel.close().wait()
}
And the logout()
method:
@objc(AuthClient) class AuthClient: NSObject {
// ...
@objc func logout(
_ oauthToken: String,
resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
let eventLoopGroup = PlatformSupport.makeEventLoopGroup(
loopCount: 1
)
defer {
try? eventLoopGroup.syncShutdownGracefully()
}
let authServiceClient = try self.createGrpcServiceClient(
eventLoopGroup: eventLoopGroup
)
// build the OauthCredentials object
let oauthCredentials: AuthService_OauthCredentials = .with {
$0.token = oauthToken
}
// grab the logout() method from the gRPC client
let call = authServiceClient.logout(oauthCredentials)
// execute the gRPC call and grab the result
do {
let logoutResult = try call.response.wait()
print("Logged out")
resolve([
"token": logoutResult.token,
"timeoutSeconds": logoutResult.timeoutSeconds]
)
} catch {
let error = NSError(domain: "", code: 200, userInfo: nil)
reject("0", "RPC method ‘logout’ failed", error)
}
}
try? authServiceClient.channel.close().wait()
}
Export Methods through the Bridge
Now that you have the gRPC client code and the Objective-C bridge, you’ll need to push those client methods through the bridge to expose them to the React Native native module system.
To do this, you must create a new Objective-C file (Cmd+n), and name it the same as your previous Swift file (in this case, AuthClient.m
):
In order to export a class to React Native, you must include the RCTBridgeModule.h
framework in this file.
The export syntax goes like this:
@interface RCT_EXTERN_MODULE(ClassName, NSObject)
RCT_EXTERN_METHOD(methodWithouParams)
RCT_EXTERN_METHOD(
asyncMethodWithoutParams: (RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock) reject
)
RCT_EXTERN_METHOD(
asyncMethodWitParams: (NSString *) param1
param2Name: (NSString *) param2
resolve: (RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock) reject
)
@end
We will be exporting a class AuthClient
, which has three methods: init()
, login()
and logout()
, so the AuthClient.m
file looks like this:
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"@interface RCT_EXTERN_MODULE(AuthClient, NSObject)
RCT_EXTERN_METHOD(init)
RCT_EXTERN_METHOD(
login: (NSString *)username
password:(NSString *)password
resolve: (RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
logout: (NSString *) oauthToken
resolve: (RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock) reject
)
@end
Now is a great time to build in your Terminal again to make sure everything is still working:
$ npx react-native run-ios
6. Build React Native Native Module for gPRC Client
Now that you have the Swift gRPC client built and bridged through Objective-C, it’s time to build the corresponding Javascript/React Native library.
For this tutorial, we won’t create any components or views. We will just edit the App.js
to keep things simple.
In your App.js
, import the NativeModules
module and then the AuthClient
that was exposed through the bridge:
import { NativeModules } from "react-native"
const { AuthClient } = NativeModules
Now you can load the AuthClient
in the app on first run:
import React, { useState, useEffect } from 'react'
import { NativeModules } from 'react-native'
const { AuthClient } = NativeModules// ...const App: () => Node = () => {
const [isFirstRun, setIsFirstRun] = useState(true)
useEffect(() => {
if (isFirstRun) {
AuthClient.init()
setIsFirstRun(false)
}
}, [isFirstRun])
// ...
}
Let’s also implement the login()
and logout()
methods. We will store the authentication token so we can display it to the user:
// ...
const App: () => Node = () => {
const [username, setUsername] = useState('email@example.com')
const [password, setPassword] = useState('password')
const [oauthToken, setOauthToken] = useState('') const login = async (username, password) => {
try {
const oauthData = await AuthClient.login(username, password)
setOauthToken(oauthData.token)
} catch (error) {
console.log(error)
console.log(error.message)
}
}
const logout = async (oauthToken) => {
await AuthClient.logout(oauthToken)
setOauthToken(null)
}
// ...
}
Now is a great time to build to make sure everything still works :-)
7. Integrate with UI
Now that you have the logic, you can throw up a simple UI to connect everything.
This shouldn’t be anything too new if you are reading this, so I won’t spend any time on the implementation. It’s subject to your implementation anyway.
import {
SafeAreaView,
View,
TextInput,
Text,
Button
} from 'react-native'// ...const App: () => Node = () => {
// ...
return (
<SafeAreaView style={styles.backgroundStyle}>
<TextInput
type="email"
placeholder="Email"
autoCapitalize="none"
value={username}
onChangeText={setUsername}
style={styles.textInput}
/>
<TextInput
type="password"
placeholder="Password"
autoCapitalize="none"
value={password}
onChangeText={setPassword}
style={styles.textInput}
/>
<Button
title="Login"
onPress={() => { login(username, password) }}
/>
<Button
title="Logout"
onPress={() => { logout(oauthToken) }}
/>
<Text>Oauth Token: {oauthToken}</Text>
</SafeAreaView>
)
}
8. Build and Test
As always, build and test. Make sure you are running your gRPC server of course.
$ npx react-native run-ios
That’s it! You should have a working gRPC client in React Native on your iOS Simulator!
Further Reading
Building a gRPC Client in iOS / Swift
React Native Native Modules in iOS / Swift
React Native Native Modules Official Documentation.
TeaBreak has a a fabulous tutorial about how to return various types of data from Swift Methods in Swift in React Native — The Ultimate Guide Part 1: Modules.