NCMBのSwift SDKとWebSocketを使ってチャットアプリを作る（その2：メッセージの送信とデータストアへの保存）

## ハンズオン開催

こちらの内容を元にオンラインハンズオンを開催します。1時間程度になりますので、ご興味があればぜひご参加ください。

- [11月09日13時〜 Swift × NCMBでチャットアプリを作ろう - connpass](https://ncmb.connpass.com/event/228559/)
- [11月11日17時〜 Swift × NCMBでチャットアプリを作ろう - connpass](https://ncmb.connpass.com/event/228560/)

NCMBのSwift SDKを使ってデモアプリを作ってみます。リアルタイム通信系は人気があるのですが、NCMBでは残念ながらWebSocketは使えません。そこで今回はPieSocketというWebSocketを提供するサービスと組み合わせて、Swift製のチャットアプリを作ってみます。

[前回は仕様と画面の説明、Swift SDKの初期化までを説明しました](https://qiita.com/goofmint/items/085f2a2b414cf52f1daa)。今回はチャットメッセージの送信について解説します。

## コードについて

今回のコードは[NCMBMania/Swift_Chat_Demo](https://github.com/NCMBMania/Swift_Chat_Demo)にアップロードしてあります。実装時の参考にしてください。

## チャットメッセージの送信

フローは次のようになります。

- NCMBのデータストアにチャットメッセージを保存する
- WebSocketでメッセージを送信する

### NCMBのデータストアにチャットメッセージを保存する

入力されたテキストと、ログインユーザの情報を紐付けてNCMBに保存します。これは ChatInputView の send メソッドに実装します。注意点としてはACLを使ってアクセスコントロールを設定しているところでしょうか。

```swift
// チャットメッセージを送信する関数
func send() {
    // NCMBのデータストア用のオブジェクトを用意
    let obj = NCMBObject(className: "Chat")
    // 必要なデータを設定
    obj["body"] = message
    let user = NCMBUser.currentUser
    obj["userId"] = user!.objectId
    var acl = NCMBACL.empty // ACL（アクセス管理）を定義
    // * = 全員。読み込み可能、編集不可
    acl.put(key: "*", readable: true, writable: false)
    // ユーザのobjectId。指定したユーザ（投稿者）のみ読み込み可能、編集可能
    acl.put(key: user!.objectId!, readable: true, writable: true)
    obj.acl = acl // ACLを設定
    // 表示名も設定
    if let displayName: String = user!["displayName"] {
        obj["displayName"] = displayName
    }
    _ = obj.save()       // 保存
    message = ""         // 入力されていたメッセージを消す
    chat.send(obj: obj)  // WebSocketで送信
}
```

### WebSocketでメッセージを送信する

保存後、WebSocketでメッセージを送信します。WebSocketでは基本的にテキストメッセージを送信するので、NCMBObjectから必要なデータを取り出して、JSONテキストに変換します。これは ChatScreenModel の `makeMessage` メソッドで行います。

```swift
// チャットメッセージを送信する関数
func send(obj: NCMBObject) {
    // NCMBObjectからメッセージを作成して、送信
    webSocketTask?.send(.string(makeMessage(obj: obj)), completionHandler: { error in
        if error != nil {
            // エラーの場合
            print(error)
        }
    })
}
```

`makeMessage` は必要な情報を集めてDictionaryを作成し、それをJSONテキスト化します。日付だけISO8601形式に変換して入れています。

```swift
// NCMBObjectをDictionaryにして、JSON文字列にする関数
private func makeMessage(obj: NCMBObject) -> String {
    // Dictionaryの準備
    var json = Dictionary<String, String>()
    json["objectId"] = obj.objectId! // チャットメッセージのobjectId
    json["body"] = obj["body"] ?? "" // チャットメッセージ
    json["userId"] = obj["userId"] ?? "" // チャットの送信者
    json["displayName"] = obj["displayName"] ?? "" // チャットの表示名
    // 投稿日時
    let formatter = ISO8601DateFormatter()
    json["createDate"] = formatter.string(from: Date())
    do {
        // Dictionaryを文字列化
        let jsonData = try JSONSerialization.data(withJSONObject: json)
        return String(bytes: jsonData, encoding: .utf8)!
    } catch (let e) {
        print(e)
    }
    return "" // エラーの場合
}
```

WebSocketでメッセージを送信すると受信時のハンドラである `onReceive` メソッドが呼ばれます。ここでは送信時と逆に、受け取ったメッセージを分解してNCMBObjectオブジェクトを作成します。なお、URLSessionWebSocketTaskではメッセージを1度しか受信してくれないので、最後にもう一度受信時のハンドラを設定しておきます。

```swift
// メッセージを受け取った時に呼ばれる関数
private func onReceive(incoming: Result<URLSessionWebSocketTask.Message, Error>) {
    switch incoming {
    case let .success(message): // 正しく受け取れている場合
        // メッセージの種類に応じて処理分け（今回はテキストのみ）
        switch message {
        case let .string(msg):
            // テキストをデータ化
            let data: Data =  msg.data(using: String.Encoding.utf8)!
            do {
                // JSONとしてパース
                let params = try JSONSerialization.jsonObject(with: data) as! Dictionary<String, String>
                // NCMBObjectとして作り直し
                let obj = NCMBObject(className: "Chat")
                obj["displayName"] = params["displayName"]
                obj.objectId = params["objectId"]
                obj["userId"] = params["userId"]
                obj["body"] = params["body"]
                obj["createDate"] = params["createDate"]
                DispatchQueue.main.async {
                    self.messages.append(obj) // メッセージの配列に追加
                }
            } catch {
            }
            break
        case let .data(data):
            print(data)
        @unknown default:
            print("unknown \(message)")
        }
        break
    case let .failure(err):
        print(err)
        break
    }
    webSocketTask?.receive(completionHandler: onReceive)
}
```

これでメッセージの送信処理が完了します。

## まとめ

今回はチャットメッセージの送信と、NCMBのデータストアへの保存について解説しました。次回はチャットメッセージの表示と、過去のメッセージを取得する部分を解説します。