NCMBのSwift SDKとWebSocketを使ってチャットアプリを作る（その3：チャットの表示とチャットデータの取得）

## ハンズオン開催

こちらの内容を元にオンラインハンズオンを開催します。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製のチャットアプリを作ってみます。

[前回はチャットメッセージの送信について解説](https://qiita.com/goofmint/items/4d2aa88035fc48e74b17)しました。今回はチャットメッセージの表示と、既存データの取得について解説します。

## コードについて

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

## チャットメッセージの表示

チャットメッセージの表示は `ChatView` にて行います。WebSocketの接続やメッセージの送受信を管理している `ChatScreenModel` を `ObservedObject` として定義しています。

```swift
struct ChatView: View {
    @ObservedObject var chat = ChatScreenModel()
```

そして `chat.messages` にメッセージが追加されたらリストを更新しています。この時、ForEach の id は objectId になります。

```swift
ScrollView {
    ScrollViewReader { proxy in
        LazyVStack(spacing: 8) {
            ForEach(chat.messages, id: \.objectId ) { message in
                ChatMessageRow(message: message)
            }
        }
        .onChange(of: chat.messages.count) { _ in
            scrollToLastMessage(proxy: proxy)
        }
    }
}
```

`scrollToLastMessage` はリストの最後のメッセージを追いかける形で自動スクロールするためのメソッドです。

```swift
// 自動スクロール用
private func scrollToLastMessage(proxy: ScrollViewProxy) {
    if let lastMessage = chat.messages.last {
        withAnimation(.easeOut(duration: 0.4)) {
            proxy.scrollTo(lastMessage.objectId, anchor: .bottom)
        }
    }
}
```

### 自分と相手のメッセージで表示を分ける

自分のメッセージか否かで表示（色や配置）を分けています。これは `ChatMessageRow` で実装しています。自分のメッセージかどうかはNCMBObjectにあるuserId（チャット投稿者のobjectId）を使って判定しています。

```swift
struct ChatMessageRow: View {
    @State var message: NCMBObject
    // 日付のフォーマット（時刻のみ）
    static private let dateFormatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateStyle = .none
            formatter.timeStyle = .short
            return formatter
        }()
    
    var body: some View {
        HStack {
            if isMe() {
                Spacer()
            }
            VStack(alignment: .leading, spacing: 6) {
                HStack {
                    Text(message["displayName"]! as String)
                        .fontWeight(.bold)
                        .font(.system(size: 12))
                    
                    Text(Self.dateFormatter.string(from: createDate()))
                        .font(.system(size: 10))
                        .opacity(0.7)
                }
                
                Text(message["body"]! as String)
            }
            .foregroundColor(isMe() ? .white : .black)
            .padding(10)
            .background(isMe() ? Color.blue : Color(white: 0.95))
            .cornerRadius(5)
            
            if !isMe() {
                Spacer()
            }
        }
    }
    
    // 自分宛かどうか判定する関数
    func isMe() -> Bool {
        if let userId: String = message["userId"] {
            let user = NCMBUser.currentUser
            return userId == user!.objectId
        }
        return false
    }
    
    // 日付をフォーマットに沿って返す
    func createDate() -> Date {
        if let createDate: String = message["createDate"] {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
            return dateFormatter.date(from: createDate)!
        }
        return Date()
    }
}
```


## 既存メッセージの取得

![Simulator Screen Shot - iPod touch (7th generation) - 2021-09-08 at 13.54.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e07e317d-31d0-9bd1-fb57-7618a2557c9d.png)


過去にやり取りされたメッセージは NCMB のChatクラスにあります。チャット画面が表示されたタイミングで、そのデータを取得します。データを受け取ったらメインスレッドで更新する必要があるので注意してください。メッセージはそのまま `chat.messages` に入れてしまえば、表示に反映されます。

```swift
// NCMBに保存されているメッセージを取得する関数
func getPastMessages() {
    // データ取得用のクエリオブジェクトを用意
    var query = NCMBQuery.getQuery(className: "Chat")
    // 並び順はcreateDateの昇順
    query.order = ["createDate"]
    // 20件取得
    query.limit = 20
    // 検索実行
    query.findInBackground(callback: { result in
        // 結果判定
        switch result {
        case let .success(ary):
            // 取得できた場合は結果をチャットメッセージとして反映
            DispatchQueue.main.async {
                chat.messages = ary
            }
            break
        case .failure(_): break // エラーの場合
        }
    })
}
```

## 反映をリアルタイムで行う

メッセージが追加された際に表示への反映をリアルタイムで行うため `ChatScreenModel` の `messages` に `@Published` を付けておきます。

```swift
final class ChatScreenModel: ObservableObject {
    private var webSocketTask: URLSessionWebSocketTask?
    @Published var messages: [NCMBObject] = []
```

これで過去のメッセージも表示できるようになります。

## まとめ

今回はNCMBの匿名認証とデータストア、そしてPieSocketを使ってチャットアプリを作りました。今回はチャンネルIDを1固定にしていますが、これを変えることで複数のチャットルームにも対応します。ぜひカスタマイズして、Swiftアプリの中にチャット機能を組み込んでください。

[ドキュメント : 開発者向けドキュメント | ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/index.html#/Swift)