iOS SDK(GTNContactSDK)
GTN Contact の顧客向けチャットをネイティブiOSアプリに組み込むための SwiftUI SDK です。会話一覧・チャット・添付・CSATアンケートまでを含む画面一式を提供します。
概要
| 項目 | 内容 |
|---|---|
| 最小OS | iOS 17+ |
| 言語 | Swift 6(strict concurrency 対応、@MainActor / Sendable 準拠) |
| UI | OS標準SwiftUI(SF Symbols・システムカラー)。文言は日本語 |
| 依存ライブラリ | ゼロ(URLSession / URLSessionWebSocketTask / Codable のみ) |
| 配布形態 | Swift Package(sdks/ios/GTNContactSDK) |
インストール(Swift Package Manager)
Xcodeの File > Add Package Dependencies… で sdks/ios/GTNContactSDK ディレクトリをローカルパッケージとして追加するか、Package.swift に依存を記述します。
dependencies: [
.package(path: "../GTNContactSDK")
]
クイックスタート
1. SDKを初期化(configure)
アプリ起動後・サポートUI表示前に一度だけ GTNContact.configure を呼び出します。
import GTNContactSDK
GTNContact.configure(
baseURL: URL(string: "https://contact.example.com")!,
tokenProvider: {
// ホストアプリの認証バックエンドから顧客トークンを取得して返す
try await MyAuthBackend.fetchSupportToken()
},
language: "ja"
)
2. トークン供給(tokenProvider)について
- SDKは絶対に自分でトークンを発行しません。
tokenProvider(@Sendable () async throws -> String)でホストアプリが顧客トークンを供給します。 - 取得したトークンはSDK内部でキャッシュされ、RESTとWebSocketの両方に使われます。
- devモードのトークン形式は
dev:<customerId>です(サンプルアプリではPOST /v1/guestで自前取得)。
3. サポート画面を表示
SwiftUIから(推奨):
.sheet(isPresented: $showSupport) {
SupportHomeView(
sourceScreen: "請求明細画面", // 起動元画面名 → サポートフローが自動解決される
context: ["契約プラン": "プレミアム"], // フローに渡す追加コンテキスト
showsCloseButton: true
)
}
UIKitなどからモーダル起動:
GTNContact.presentSupport(sourceScreen: "ホーム画面", context: ["契約プラン": "プレミアム"])
認証
tokenProvider と 401 リトライ
- 全リクエストに
Authorization: Bearer <token>が付与されます。 - APIが 401 を返した場合、SDKはキャッシュを破棄して
tokenProviderを再呼び出しし、1回だけリトライします。再取得後も401ならGTNContactError.unauthorizedを投げます。 - トークン失効時の再取得(リフレッシュ)はホストアプリの責務です。
tokenProviderは常に「現在有効なトークン」を返すよう実装してください。
ゲスト→ログイン統合(claimGuest)
ゲスト時代の会話をログイン後の顧客に統合します(POST /v1/identities/claim)。tokenProvider がログイン後のトークンを返すようになった後に呼び出してください。
let mergedCount = try await GTNContact.claimGuest(guestCustomerId: guestId) // 統合された会話数
プッシュトークン登録
// APNsデバイストークン(16進文字列)の登録(POST /v1/push-tokens、platform: "ios")
try await GTNContact.registerPushToken(deviceTokenHex)
// ログアウト時などに解除(DELETE /v1/push-tokens)
try await GTNContact.unregisterPushToken(deviceTokenHex)
画面の埋め込み方
| エントリポイント | 用途 |
|---|---|
SupportHomeView(sourceScreen:context:showsCloseButton:) | 会話一覧+新規問い合わせ。SwiftUIの sheet や NavigationStack に直接埋め込む |
GTNContact.presentSupport(sourceScreen:context:) | UIKit等からの最短導線。keyWindowの最前面VCに pageSheet でモーダル表示 |
ChatView(conversationId:initialMessages:conversation:) | 特定の会話のチャット画面を直接開く(プッシュ通知タップからの遷移など) |
// 特定の会話を直接開く
ChatView(conversationId: conversationId)
機能リファレンス
会話
- 会話一覧(
GET /v1/conversations): 新しい順表示、プルリフレッシュ - 会話開始(
POST /v1/conversations):sourceScreenでフローが自動解決され、チケットも自動作成されます - ステータスは5種類で、日本語バッジ表示されます
| status | 表示 |
|---|---|
bot | ボット対応中 |
queued | 接続待ち |
agent | 対応中 |
waiting_customer | 返信待ち |
closed | 終了 |
構造化メッセージ(7種)
MessageBubbleView が contentType に応じてレンダリングします。
| contentType | 内容 | 挙動 |
|---|---|---|
text | テキスト | 通常バブル |
buttons | 選択肢ボタン | タップで value をtext送信。最新メッセージのみ有効、selected は押下不可 |
form | 入力フォーム | 最新のみ編集可、過去分は読み取り専用 |
carousel | カルーセル | 横スクロールカード |
file | ファイル | タップでダウンロードSAS取得→表示 |
link_card | リンクカード | 外部ブラウザで開く |
system | システム通知 | 中央寄せグレー小文字 |
差分同期とページング(tail / before / after)
メッセージの seq は会話内で単調増加するカーソルです。GET /v1/conversations/{id}/messages は3つのクエリを受け付けます。
| パラメータ | 用途 | レスポンス |
|---|---|---|
tail=1 | 初回表示: 最新50件(limit 指定可)を取得 | prevCursor(次の before に渡すseq)と hasMoreBefore が返る |
before=<prevCursor> | 上スクロール: 過去メッセージを遡って取得 | 同上(さらに古い分があれば hasMoreBefore: true) |
after=<maxSeq> | 差分同期: 保持している最大seqより新しい分だけ取得 | nextCursor(次の after に渡すseq)と hasMore |
推奨パターン: 初回は tail=1 で最新50件 → 上スクロールで before=prevCursor を繰り返して遡り → 新着・再接続時は after=maxSeq で差分同期。
内蔵の ChatView は3つのカーソルをすべて使い分けます: 初回表示は getLatestMessages(tail=1)で最新50件のみを取得して最下部を表示し、上スクロールで先頭に到達すると getMessagesBefore(before=prevCursor)で古いページを追加読み込みします(スクロール位置は維持)。新着・再接続時の取りこぼし回復は従来どおり after=maxSeq の差分同期+id による重複排除です。REST APIを直接呼ぶ場合も同じ3カーソルが利用できます。
WebSocket(リアルタイム)
SocketClient が会話単位で GET /v1/ws?conversationId=<id>&token=<bearer token> に接続し、AsyncStream<SocketEvent> でイベントを配信します。
| イベント | 内容 |
|---|---|
connected | 接続確立(再接続含む)。受信側は after=maxSeq で差分同期し、readを再送すること |
disconnected | 切断(自動再接続が走る) |
messageCreated | 新着メッセージ。自分の送信もエコーされるため id で重複排除する |
conversationUpdated | status変更・エージェント参加など |
typing | 入力中通知(SDKは送信側500msデバウンス、受信時「入力中…」表示) |
read | 既読通知(senderType: agent なら自分のメッセージに既読表示) |
presence | 閲覧者一覧(顧客UIでは未使用) |
reactionUpdated | 絵文字リアクションの追加/削除。対象メッセージの全リアクションのスナップショットが届くため丸ごと置き換える |
- 自動再接続: 指数バックオフ 1s → 最大10s。再接続時は差分取得+既読再送を行います。
- 送信系:
sendTyping(isTyping:)/sendRead(lastSeq:)。画面破棄時は必ずclose()を呼びます。
送達・既読
- 送信は
clientMessageIdでべき等。ネットワークエラー時は同一IDで最大3回自動リトライし、失敗時はタップで再送できます。 - 表示は 送信中(時計)→ 送達(チェック)→ 既読(「既読」表示)の3段階。既読は会話詳細の
readsとWSのreadイベントで更新されます。
絵文字リアクション
- バブルの長押しでクイックリアクション(👍 🙏 ❤️ 😂 🎉 😢)を付けられます。バブル下のチップをタップすると自分のリアクションをトグルできます。
- メニューの「その他の絵文字…」から全絵文字ピッカー(MCEmojiPicker。SDKの依存としてSPMで自動解決)を開き、任意の絵文字を選べます(カテゴリ・肌色バリアント対応)。
- API:
addReaction(conversationId:messageId:emoji:)/removeReaction(conversationId:messageId:emoji:)(POST/DELETE /v1/conversations/{id}/messages/{messageId}/reactions)。追加は同一絵文字の再送に対してべき等です。 - どちらも対象メッセージの全リアクション(スナップショット)を返します。他の参加者の変更はWSの
reactionUpdatedで届きます。 - ViewModelでは
toggleReaction(for:emoji:)/hasMyReaction(_:emoji:)を利用できます(会話は単一顧客のためreactorType == .customerが自分の判定)。
添付(SASフロー)
createAttachmentUpload(conversationId:fileName:contentType:)—POST /v1/conversations/{id}/attachmentsでアップロード用SASを発行(15分有効)uploadAttachment(sas:data:)— 返却されたuploadUrlへheaders(x-ms-blob-type等)付きで Azure Blobへ直接PUTsendMessage(contentType: .file)—blobName/fileName/fileSizeを含むfileメッセージを送信- 閲覧時は
attachmentDownloadURL(conversationId:blobName:)でダウンロード用SAS(60分有効)を取得
内蔵の AttachmentPicker はPhotosPicker+fileImporterでこのフローを自動実行します。
CSAT(満足度アンケート)
- 会話クローズ後、
CsatFormViewがアンケートを表示します(1会話1回。回答済みはローカルにも記録)。 - 設問は
csatQuestions()(GET /v1/csat/questions、有効設問のみorder順)で取得。設問タイプはstar5/binary/nps/textの4種で、必須バリデーション付き。 - 回答は
submitCsat(conversationId:score:comment:answers:)(POST /v1/conversations/{id}/csat)で送信します(クローズ後のみ)。
エラーハンドリング
エラーは GTNContactError(LocalizedError 準拠)で投げられ、errorDescription はそのままユーザーに表示できる日本語です。
| ケース | 発生条件 | 日本語メッセージ |
|---|---|---|
notConfigured | configure 未呼び出し | SDKが初期化されていません。GTNContact.configure を呼び出してください。 |
network(underlying:) | ネットワーク到達不可・タイムアウト | 通信に失敗しました。ネットワーク接続を確認して、もう一度お試しください。 |
unauthorized | トークン再取得後も401 | 認証に失敗しました。しばらくしてからもう一度お試しください。 |
conversationClosed | 409 conversation_closed | この会話は終了しているため、メッセージを送信できません。 |
csatAlreadySubmitted | 409 already_submitted | このアンケートにはすでに回答済みです。 |
conversationNotClosed | 409 conversation_not_closed | 会話が終了していないため、アンケートに回答できません。 |
server(status:code:detail:) | その他のサーバーエラー | エラーが発生しました(code): detail |
decoding(underlying:) | レスポンスの解析失敗 | サーバー応答の解析に失敗しました。 |
409 コード一覧
| HTTP | error コード | 意味 |
|---|---|---|
| 409 | conversation_closed | クローズ済み会話へのメッセージ送信 |
| 409 | already_submitted | CSATにすでに回答済み |
| 409 | conversation_not_closed | 未クローズ会話へのCSAT回答 |
サンプルアプリの起動方法
- APIサーバーをローカルで起動(
http://localhost:3010) sdks/ios/SampleAppを開いてビルド:
xcodebuild -project SampleApp.xcodeproj -scheme SampleApp
- アプリ内の「デモログイン」は
POST /v1/guestでトークンを自前取得してSDKに供給します(=ホストアプリの認証バックエンドの代役)。 - UIテスト
SampleAppUITestsがログイン→会話開始→ボタン選択→フォーム送信まで自動実行します。
FAQ
ローカルAPI(localhost)に接続できない
http://localhost:3010 など平文HTTPへ接続する場合、ホストアプリの Info.plist に App Transport Security の例外が必要です。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
Auth0 接続時に変更は必要?
SDK側のコード変更は不要で、tokenProvider の実装だけが変わります。
- devモード(サーバーの
ALLOW_DEV_AUTHが有効)ではdev:<customerId>形式のトークンをそのまま渡せます。 - サーバーに
AUTH0_DOMAINが設定されている場合、tokenProviderは Auth0が発行した顧客用JWT(AUTH0_CUSTOMER_AUDIENCEのaudience)を返すようにします。エージェント用audienceとは分離されています。 - トークン失効時はSDKの401リトライで
tokenProviderが再呼び出しされるので、そこでリフレッシュ済みトークンを返してください。 - ゲスト運用から移行する場合は、ログイン完了後に
GTNContact.claimGuest(guestCustomerId:)でゲスト時代の会話を統合できます。