第7回は、第4回で実装したサーバサイドストリーミングgRPCサービスを利用するモバイルアプリケーションを、iOS用にSwiftで開発します。前回と同様に、gRPCとモバイルアプリケーションの相性などを理解し、異なるプラットフォームとプログラミング言語で構成されるサービスを問題なく利用できることを理解します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
今回のテーマは、プログラミング言語SwiftによるgRPCクライアントiOSアプリの開発です。連載第4回で、PythonによるサーバサイドストリーミングのgRPCサービスを開発し、前回はそのサーバにAndroidアプリからクライアントとしてアクセスしました。今回は、これをiOSアプリに置き換えてアクセスします。連載第1回で紹介したように、gRPCのサービスはプログラミング言語やプラットフォームに依存しない設計となっています。今回は、サーバがPython、クライアントがiOSとして、問題なく利用できることを確認します。
サーバサイドストリーミング、そしてPythonによるgRPCサービスの開発については、連載第4回を参照してください。サーバも、連載第4回のものをそのまま利用します。
最初に、開発に必要な環境を準備しておきます。今回はクライアントがiOSアプリとなるので、主にiOSアプリの開発環境を作っていきます。サーバは同一ホストで動作させます。iOSアプリの開発には幾つかの方法がありますが、今回はgRPC公式リポジトリから入手できるgrpc-swiftをSwiftパッケージとして利用し、標準の開発ツールXcodeで開発していきます。以下の、必要なソフトウェアをインストールしておいてください。
Xcodeのインストールは、macOSのDockにあるXcodeのアイコンをクリックして実行します。以下のウィンドウでは、Built-inと表示されているもの以外は不要なので、そのまま[Install]をクリックしてください。インストール済みの場合は、アイコンのクリックでそのままXcodeが起動します。
swift-protobufとgrpc-swiftは、protocがSwiftのコードを生成するために必要なプラグイン(protoc-gen-swiftとprotoc-gen-grpc-swift)です。protocは、連載第1回で触れたProtocol Buffersのツールで、プロトコル定義ファイルをコンパイルして言語ごとのクラスファイルなどを作成します。protocは標準でSwiftに対応していないので、このような外部のプラグインを必要とします。swift-protobuf、grpc-swiftはHomebrewで以下のようにインストールできます(protocを含むprotobufは、依存関係から自動的にインストールされます)。
% brew install swift-protobuf grpc-swift
開発環境が用意できたら、ベースとなるiOSアプリを作成して実行してみましょう。このアプリに、gRPCクライアントの機能を画面レイアウトの変更とともに実装していきます。
本連載で共通して利用するフォルダatmarkit_grpcに、プロジェクトを配置するフォルダ(swiftなど)を作成しておきます。atmarkit_grpcフォルダに直接プロジェクトを配置してもよいのですが、Xcodeのプロジェクトはプロジェクトファイル(.xcodeproj)と同名のサブフォルダから構成されるので、他の回のサンプルのようにatmarkit_grpcフォルダにはフォルダのみが置かれるように階層を一つ深くしています。
Xcodeを起動します。Xcodeの起動直後に表示されるWelcome画面で、[Create a new Xcode project]をクリックします。この画面が出ない場合は[Command]+[Shift]+[1]を入力します。あるいは、メニューから[File]−[New]−[Project...]を選択します。
プロジェクトのテンプレートを尋ねられますので、[iOS]と[App]をクリックして選択し、[Next]をクリックします。
プロジェクトのオプションを尋ねられますので、それぞれ以下の表のように入力、選択して[Next]をクリックします。
項目 | 概要 |
---|---|
Project Name | プロジェクトの名称(gRPCClientなど) |
Team | None(Apple IDでサインインしても可) |
Organization Identifier | ドメイン(jp.naosanなど独自のものを指定) |
Interface | SwiftUI |
Language | Swift |
Use Core Data | Core Dataを使用するか(チェックなし) |
Include Tests | テストコードを含むか(チェックなし) |
表1 プロジェクトのオプション |
プロジェクトを作成するフォルダを選択するようになりますので、作成したフォルダ(例えばswift)を選択して[Create]をクリックします。プロジェクト単体でのGitHub連携は行わないので、ローカルリポジトリを作成するための[Create Git repository on my Mac]のチェックは不要です。
プロジェクトの生成後、しばらく経って以下のようにソースコードやプレビューが表示されれば成功です。ウィンドウ上部の表示で分かるように、最新のデバイスを想定したiOSシミュレーターもこの時点で準備されています(この場合はiPhone 14 Pro、iOS 16.4)。見た目の確認はプレビューで行えますが、操作を伴う確認はこのシミュレーターを使います。なお、本稿では実機にインストールしての確認を割愛します。
作成したプロジェクトは、直ちに実行できます。ウィンドウ上部の実行ボタンをクリックするか、[Command]+[R]を入力します。あるいは、メニューから[Product]−[Run]を選択します。初回の実行はかなり時間がかかりますが、しばらく待ちましょう。以下のようなiOSシミュレーターの画面が表示されれば成功です。
シミュレーターの停止は、同じくウィンドウ上部の停止ボタンをクリックするか、[Command]+[.]を入力します。あるいは、メニューから[Product]−[Stop]を選択します。
既定のiOSアプリの動作確認が済んだところで、BookInfoサービスのクライアントアプリとして改変していきます。作業の内容は、gRPCサービスの利用に必要なSwiftパッケージの追加、BookInfoサービスのためのファイルの追加、そして既存のファイルを修正していくというイメージです。
gRPCサービスに必要なライブラリなどは、全て公式のgrpc-swiftリポジトリにあります。このリポジトリは、そのままSwiftパッケージになっていますので、プロジェクトへの追加が容易です。[File]−[Add Packages...]を選択して、パッケージ追加ウィンドウを開きます。
既定で、「Apple Swift Packages」が11個利用可能となっています。中央ペインには、「SwiftProtobuf」や「swift-nio」といった今回の目的に必要なパッケージがリストされていますが、grpc-swiftリポジトリから直接パッケージを追加して、それらは依存関係で追加することにします。ウィンドウ右上の、「Search or Enter Package URL」と表示されている検索窓に、「https://github.com/grpc/grpc-swift」と入力します。すると、リポジトリからパッケージの情報が読み込まれて中央ペインの表示が変わります。
「grpc-swift」を選択して、[Add Package]をクリックします。しばらくすると、利用するプロダクトを選択するウィンドウが表示されますので、「GRPC」と「protoc-gen-grpc-swift」にのみチェックを入れて、[Add Package]をクリックします。
しばらくすると、メインウィンドウの左ペインに、追加されたパッケージが表示されます。
Apple Swift Packageにあった、swift-nioやSwiftProtobufも追加されていることが分かります。
BookInfoサービスを追加します。プロトコル定義ファイルは連載第4回(Python)のものをそのまま使えますので、protoc経由でprotoc-gen-swiftとprotoc-gen-grpc-swiftを使って、Swiftのコードを生成します。カレントフォルダはatmarkit_grpcフォルダとしていますが、オプションに指定するパスは実際のフォルダ構成に合わせて変更してください。
% protoc --proto_path=./python/Protos --grpc-swift_out=./swift/BookInfoClient/BookInfoClient --swift_out=./swift/BookInfoClient/BookInfoClient ./python/Protos/bookinfo.proto
これで、プロジェクトのルートにbookinfo.grpc.swift、bookinfo.pb.swiftというSwiftのソースファイルが2つ作成されます。これまでの回でも触れてきたように、生成されたファイルは触る必要はありませんし、中身を見る必要もありません。ただし、これらのファイルはプロジェクトの依存関係に含まれずコンパイルされないので、プロジェクトに追加してあげます。追加は、メニューから[File]−[Add Files to "BookInfoClient"...]を選択し、bookinfo.grpc.swiftファイルとbookinfo.pb.swiftファイルを選択して[Add]をクリックします。
これで、メインウィンドウ左ペインのプロジェクトツリーにファイルが追加されているのを確認できます。
ここからは、ビューであるContentView.swiftファイルを修正していきます。先ほど実行した通り、既定では地球(globe)のイメージと"Hello, world!"が縦に並んで表示されるだけです。これらは一切不要なので削除してしまい、以下のようにContentView構造体のbodyプロパティの定義を置き換えます。また、状態保持のための幾つかのプロパティを追加します。
struct ContentView: View { // テキスト入力フィールドを受ける文字列プロパティ @State var inputText = "" (1) // 検索状況を保持する文字列プロパティ @State var str = "検索は実行されていません" // 検索結果を保持する文字列の配列プロパティ @State var items: [String] = [] // ビューに展開されるUIとしてのプロパティ var body: some View { (2) VStack { (3) // キーワード入力 Text("キーワード") TextField("キーワードを入力してボタンをタップ", text: $inputText) .padding() .border(.gray) // 検索開始ボタン Button { Task { (4) // ここにあとでボタンタップ時の処理を追加 } } label: { Text("検索開始") } .padding() // 検索結果(個数のテキストとリスト) Text(str) List(items, id:\.self) { item in Text(item) } } .padding() } }
iOSアプリでは、ビューのUIをSwiftUIという仕組みで、Swiftのコードで記述することになっています。(1)ではビューで使うプロパティを宣言していますが、(2)で宣言されているbodyプロパティにUIの中身を記述し、その内部で(1)のプロパティを参照しています。プロパティには@Stateが付与されているので、そのプロパティはbody内部で書き換えることができ、変更されたらビューも自動的に更新されます。
(3)ではVStack構造体によって、内部のコンポーネントを縦に並べています。検索キーワードに関連するテキストとテキスト入力フィールド、検索開始ボタン、そして検索結果のテキストとリストが配置されます。
なお、(4)はボタンタップ時の処理を記述する部分です。次項でビューにクライアントスタブを組み込んだら、ここにもタップ時の処理を記述していきます。
エラーが発生していなければ、以下のように変更されたプレビューが表示されているはずです。見た目の確認だけならプレビューで十分なので活用しましょう。
続けて、gRPCクライアントとしてのスタブを追加します。クライアントスタブの生成は、ある程度定型的な処理になるので、grpc-swiftにあるSources/Examples/RouteGuide/Client/RouteGuideClient.swiftファイルの内容をベースにしています(必要に応じて参照してください)。スタブオブジェクトを保持するプロパティと、その初期化のためのイニシャライザを記述します。
…略… import SwiftUI import GRPC (1) struct ContentView: View { …略… // BookInfoクライアントのオブジェクト var client: Bookinfo_MultiBookInfoAsyncClient? = nil (2) // イニシャライザ init() { (3) do { // イベントループグループを生成 let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) // チャネルを生成 let channel = try GRPCChannelPool.with( (4) target: .host("localhost", port: 50051), transportSecurity: .plaintext, eventLoopGroup: group ) // スタブオブジェクトを生成 self.client = Bookinfo_MultiBookInfoAsyncClient( (5) channel: channel ) } catch { dump(error) } } …略…
(1)は、今回追加するイニシャライザ(initメソッド)で使用するPlatformSupportなどの利用のために必要となるインポートです。grpc-swiftパッケージによって利用可能になっています。
(2)は、スタブオブジェクトを保持するプロパティです。UIとは無関係なので、@Stateは不要です。(3)はContentView構造体のイニシャライザで、内容は(2)で宣言したclientプロパティにスタブオブジェクトを生成するのみとなっています。GRPCChannelPoolの利用で例外が発生する可能性があるので、全体をdoブロックで囲っています。
(4)ではgRPCのための通信チャネルを生成していますが、hostメソッドの引数には、接続するgRPCサーバに合わせてURLとポートを記述します。
最後に(5)で、スタブオブジェクトを生成しています。ここの型は、protocによって生成されたサーバサイドストリーミングクライアントのための構造体であり、パッケージ名Bookinfoが前置されたBookinfo_MultiBookInfoAsyncClientとなっています。
ここでは見た目が変わるようなコードを追加していませんが、問題なければエラーなく先ほどと同様のプレビューが表示されているはずです。
最後に、ボタンタップ時の処理を記述していきます。ビューの作成の項で、ボタンタップ時の処理を記述しないままにしてありました。ここでは、その内容を記述します。記述する内容は、これまでのクライアントアプリケーションと同様に、検索メッセージの生成、Search手続きの呼び出し、結果メッセージからの検索結果の取り出しです。ここもある程度定型的な処理になるので、同じくgrpc-swiftにあるSources/Examples/RouteGuide/Client/RouteGuideClient.swiftファイルの内容をベースにしています(必要に応じて参照してください)。
…略… Button { Task { (1) do { (2) var request = Bookinfo_SearchRequest() (3) request.text = inputText items = [] (4) for try await response in self.client!.search(request) { (5) self.items.append(response.item.title) try await Task.sleep(nanoseconds: UInt64(1e8)) } str = String(format: "%d件、見つかりました", self.items.count) (6) } catch { dump(error) } } } label: { Text("検索開始") } …略…
(1)は既述ですが、非同期処理はTaskブロックで囲むことになっています。このため、Buttonメソッドの簡略化記法は使えずlabelオプションを明示してボタンのラベルを指定しています。
(2)は、例外の発生に対応するために全体をdoブロックで囲っています。
(3)は、検索リクエストを生成しています。ここで使用する型としては、Bookinfo_MultiBookInfoAsyncClientと同様にパッケージ名を前置したBookinfo_SearchRequestとなっています。
(4)で検索結果を格納する配列をクリアし、(5)でSearch手続きを呼び出し、結果を非同期イテレータで受け取って順番に処理しています。1回の繰り返しごとに10の8乗ナノ秒すなわち100ミリ秒のウエイトを入れています。これは、繰り返し処理を一気に行わないことでCPU負荷が上がらないようにするためです。
(6)では、見つかった件数を変数strにセットします。
これまでの処理で、ビューも更新されます。問題なければエラーなく先ほどと同様のプレビューが表示されているはずです。
プレビューに問題なければ、シミュレーターを起動して実際に検索してみましょう。その前に、連載第4回で紹介したPythonによるgRPCサーバを起動しておいてください。シミュレーターが起動したら、検索ボックスに適当な文字列を入れて[検索開始]をタップします。以下のように検索結果がリスト表示されれば成功です。
今回は、書籍情報検索サービスのクライアントを、SwiftによるiOSアプリとして開発してみました。サーバはPythonで開発したものですが、問題なく利用できることをお伝えできたのではないかと思います。
次回は、Goとgrpc-gatewayでWebブラウザをgRPCクライアントとしてみます。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.