LoginSignup
0
1

More than 1 year has passed since last update.

NCMBのSwift SDKとWebSocketを使ってチャットアプリを作る(その1:画面の説明とSDKの導入)

Last updated at Posted at 2021-09-08

ハンズオン開催

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

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

今回は画面の説明とSDKの導入までを進めます。

コードについて

今回のコードはNCMBMania/Swift_Chat_Demoにアップロードしてあります。実装時の参考にしてください。

利用技術について

今回は次のような組み合わせになっています。

  • Swift 5.4
  • Xcode 12.5.1
  • NCMB
  • PieSocket

仕様について

PieSocketはWebSocketだけを提供するので、データを保存しません。そのためWebSocketにつながっている時のメッセージは画面に表示できますが、立ち上げ直すとすべてのメッセージが消えてしまいます。そこで、NCMBのデータストアにメッセージを保存して、アプリを立ち上げた時にメッセージを再現できるようにします。

利用する機能について

チャットアプリで利用するNCMBの機能は次の通りです。

  • 認証機能
    • 匿名認証
  • データストア
    • チャットクラスへのデータ登録、一覧表示

画面について

今回はSwiftUIを以下の5つのViewに分けています。

  • ContentView
  • NameView
  • ChatView
  • ChatMessageRow
  • ChatInputView

ContentView

ログイン状態に応じて表示を分けています。

import SwiftUI
import NCMB

struct ContentView: View {
    private var user = NCMBUser.currentUser
    @State private var displayName = ""

    var body: some View {
        VStack {
            if displayName != "" {
                // 表示名が設定されている場合
                ChatView()
            } else {
                // 表示名がない場合
                NameView(displayName: $displayName)
            }
        }.onAppear() {
            setDisplayName()
        }
    }

    // 表示名を設定する関数
    func setDisplayName() -> Void {
        if let name: String = user!["displayName"] {
            displayName = name
        }
    }
}

NameView

Simulator Screen Shot - iPod touch (7th generation) - 2021-09-08 at 14.37.09.png

チャット用の表示名を設定するViewです。

import SwiftUI
import NCMB

struct NameView: View {
    @Binding var displayName: String
    @State var name = ""
    var body: some View {
        VStack {
            TextField("お名前", text: $name) // 2
                .padding(10)
                .background(Color.secondary.opacity(0.2))
                .cornerRadius(5)
            Button("登録する", action: {
                update()
            })
        }
    }

    // 設定された表示名をNCMBの認証データに保存する関数   
    func update() {
        let user = NCMBUser.currentUser
        user?["displayName"] = name
        _ = user?.save()
        displayName = name
    }
}

ChatView

チャットの表示を担当するViewです。

Simulator Screen Shot - iPod touch (7th generation) - 2021-09-08 at 13.54.42.png

import SwiftUI
import NCMB

struct ChatView: View {
    @ObservedObject var chat = ChatScreenModel()
    var body: some View {
        VStack {
            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)
                    }
                }
            }
            ChatInputView(chat: chat)
        }
        .onAppear() {
            chat.connect()
            getPastMessages()
        }
    }

    // NCMBに保存されているメッセージを取得する関数
    func getPastMessages() {
      // 2回目以降の記事で記述
    }

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

ChatMessageRow

チャットメッセージの一行分の表示を行うViewです。

import SwiftUI
import NCMB

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()
    }
}

ChatInputView

チャットのメッセージ入力を担当するViewです。

import SwiftUI
import NCMB

struct ChatInputView: View {
    @State private var message = ""
    @State var chat: ChatScreenModel

    var body: some View {
        HStack {
            TextField("Message", text: $message)
                .padding(10)
                .background(Color.secondary.opacity(0.2))
                .cornerRadius(5)

            Button(action: {
                send()
            }) {
                Image(systemName: "arrowshape.turn.up.right")
                    .font(.system(size: 20))
            }
            .padding()
            .disabled(message.isEmpty)
        }
        .padding()
    }

    // チャットメッセージを送信する関数
    func send() {
      // 2回目以降の記事で解説
    }
}

キーの管理

今回は3つのキーを利用します。

  • NCMBのアプリケーションキー
  • NCMBのクライアントキー
  • PieSocketのアプリケーションキー

コード中に直書きするのは良くないので、Property Listを使います。APIKey.plistというファイルを作成して、その中に3つのキーを定義します。

  • PieSocketApiKey
  • ApplicationKey
  • ClientKey

それぞれStringで、キーを入力します。

そしてそのファイルを扱うKeyManagerを作成します。こちらの内容はAPI keyを.plistにStringで保存して隠す(例: Google Maps Api) - Qiitaを参考にさせてもらいました。

import Foundation

struct KeyManager {

    private let keyFilePath = Bundle.main.path(forResource: "APIKey", ofType: "plist")

    func getKeys() -> NSDictionary? {
        guard let keyFilePath = keyFilePath else {
            return nil
        }
        return NSDictionary(contentsOfFile: keyFilePath)
    }

    func getValue(key: String) -> AnyObject? {
        guard let keys = getKeys() else {
            return nil
        }
        return keys[key]! as AnyObject
    }
}

このクラスを作っておけば KeyManager().getValue(key: "ApplicationKey") as! String のようにしてキーを扱えるようになります。

 WebSocket用のクラスの作成

ChatScreenModel.swiftというファイルを作成します。この内容は A simple chat app with SwiftUI and WebSockets — or: Swift in the back, Swift in the front! | by Freek Zijlmans | Medium を参考にしています。大枠としては次のようになります。メッセージを保存するため、 messages はNCMBObject(NCMBのデータ保存用オブジェクト)の配列となっています。

import Combine
import Foundation
import NCMB

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

    // WebSocket(今回はPieSocket)への接続を行います
    func connect() {
        let channelId = "1"
        let url = URL(string: "wss://free3.piesocket.com/v3/\(channelId)?api_key=\(KeyManager().getValue(key: "PieSocketApiKey")!)&notify_self")!
        webSocketTask = URLSession.shared.webSocketTask(with: url)
        // メッセージを受け取った時に呼ばれるハンドラ
        webSocketTask?.receive(completionHandler: onReceive)
        webSocketTask?.resume()
    }

    // 接続解除時に実行する関数
    func disconnect() {
        webSocketTask?.cancel(with: .normalClosure, reason: nil)
    }

    // メッセージを受け取った時に呼ばれる関数
    private func onReceive(incoming: Result<URLSessionWebSocketTask.Message, Error>) {
      // 2回目以降の記事で解説
    }

    // NCMBObjectをDictionaryにして、JSON文字列にする関数
    private func makeMessage(obj: NCMBObject) -> String {
      // 2回目以降の記事で解説
    }

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

今回のプロジェクト

今回は言語がSwift、インタフェースがSwiftUI、ライフサイクルはSwiftUI Appとしています。

SDKのインストール

FileメニューからSwift Packages > Add Package Dependencyと選択します。

ScreenShot_ 2021-08-02 9.35.23.png

出てきたダイアログでSwift SDKのGitリポジトリURLを入力します。GitHubのリポジトリでHTTPSとして取得できるもの、または下記URLになります。

ScreenShot_ 2021-08-02 9.35.56.png

https://github.com/NIFCLOUD-mbaas/ncmb_swift.git

バージョンは最新のものでかまいません。

ScreenShot_ 2021-08-02 9.36.05.png

後はFinishボタンを押せば完了です。

ScreenShot_ 2021-08-02 9.36.40.png

初期化

今回はSwiftUIを利用しています。ライフサイクルもSwiftUIです。

まずSDKをインポートします。

import SwiftUI
import NCMB

次に scenePhase を追加します。

@main
struct ChatApp: App {
    // 追加
    @Environment(\.scenePhase) private var scenePhase

後は body 内で onChange を使って初期化します。

var body: some Scene {
    WindowGroup {
        ContentView()
    }.onChange(of: scenePhase) { scene in
        switch scene {
        case .active:
            // キーの設定
            let applicationKey = KeyManager().getValue(key: "ApplicationKey") as! String
            let clientKey = KeyManager().getValue(key: "ClientKey") as! String
            // 初期化
            NCMB.initialize(applicationKey: applicationKey, clientKey: clientKey)
        case .background: break
        case .inactive: break
        default: break
        }
    }
}

匿名認証

今回は匿名認証(ID、パスワードを使わない、デバイス固有に生成したUUIDを使った認証)を利用します。そこで、初期化した後にSwift SDKの匿名認証を有効にします。

NCMBUser.enableAutomaticUser()

認証状態の確認

次に認証状態を確認する checkAuth 関数を呼び出します。内容は次の通りです。 NCMBUser.currentUser が nil の場合は認証されていないので、匿名認証を実行します。

func checkAuth() -> Void {
    // 認証データがあれば処理は終了
    if NCMBUser.currentUser != nil {
        return;
    }
    // 匿名認証実行
    _ = NCMBUser.automaticCurrentUser()
}

セッションの有効性チェック

認証されている場合でも、セッションの有効性は確認していません。ローカルにある認証データを復元している状態のためです。そこで、データストアに一度アクセスを行い、API通信の有効性を確認します。

func checkSession() -> Bool {
    var query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
    query.limit = 1 // レスポンス件数を最小限にする
    // アクセス
    let results = query.find()
    // 結果の判定
    switch results {
    case .success(_): break
    case .failure(_):
        // 強制ログアウト処理
        _ = NCMBUser.logOut()
        return false
    }
    return true
}

上記コードでログアウト処理を実行していますが、これは必ず失敗します。セッションが無効になっているため、ログアウトAPIの実行もまた、失敗するためです。そこで NCMB/NCMBUser.swift を開いて logOutInBackground を次のように修正します。

public class func logOutInBackground(callback: @escaping NCMBHandler<Void>) -> Void {
    NCMBLogoutService().logOut(callback: {(result: NCMBResult<NCMBResponse>) -> Void in
        // レスポンスに関係なくログアウト処理
        deleteFile()
        _currentUser = nil
        switch result {
            case .success(_):
                callback(NCMBResult<Void>.success(()))
                break
            case let .failure(error):
                callback(NCMBResult<Void>.failure(error))
                break
        }
    })
}

最終的にChatAppの内容は次のようになります。

struct ChatApp: App {
    @Environment(\.scenePhase) private var scenePhase
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { scene in
            switch scene {
            case .active:
                // キーの設定
                let applicationKey = KeyManager().getValue(key: "ApplicationKey") as! String
                let clientKey = KeyManager().getValue(key: "ClientKey") as! String
                // 初期化
                NCMB.initialize(applicationKey: applicationKey, clientKey: clientKey)
                NCMBUser.enableAutomaticUser()
                checkAuth()
                if checkSession() == false {
                    checkAuth()
                }
            case .background: break
            case .inactive: break
            default: break
            }
        }
    }

    func checkAuth() -> Void {
        // 認証データがあれば処理は終了
        if NCMBUser.currentUser != nil {
            return;
        }
        // 匿名認証実行
        _ = NCMBUser.automaticCurrentUser()
    }

    // セッションの有効性をチェックする関数
    func checkSession() -> Bool {
        var query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
        query.limit = 1 // レスポンス件数を最小限にする
        // アクセス
        let results = query.find()
        // 結果の判定
        switch results {
        case .success(_): break
        case .failure(_):
            // 強制ログアウト処理
            _ = NCMBUser.logOut()
            return false
        }
        return true
    }
}

NCMBの管理画面を修正

最後にNCMBの管理画面でアプリ設定を開き、匿名認証を有効にします。

FireShot_Capture_219_-_ニフクラ_mobile_backend_-_console_mbaas_nifcloud_com_15-23-35.jpg

これでSwift SDKの初期化と匿名認証処理が完了になります。

まとめ

今回はチャットアプリの仕様と画面、NCMBの初期化までを解説しました。次はチャットメッセージの送信と、NCMBのデータストアへの保存について解説します。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1