Slide 1

Slide 1 text

サーバーと同期してリアルタイム に更新する画面を実装する iOSDC2022 @rizumi

Slide 2

Slide 2 text

Cluster, Inc. All Rights Reserved. 2 自己紹介 Ryo Izumi @rizumi_jp クラスター株式会社 Software Engineer

Slide 3

Slide 3 text

Cluster, Inc. All Rights Reserved. 3 clusterの紹介

Slide 4

Slide 4 text

Cluster, Inc. All Rights Reserved. 4 outroom inroom

Slide 5

Slide 5 text

5 アプリの画面更新

Slide 6

Slide 6 text

Cluster, Inc. All Rights Reserved. 6 アプリの画面更新 DB API Server Client ● Serverからデータを取得して表示する画面

Slide 7

Slide 7 text

Cluster, Inc. All Rights Reserved. 7 アプリの画面更新 DB API Server Client ● Serverからデータを取得して表示する画面 fetch

Slide 8

Slide 8 text

Cluster, Inc. All Rights Reserved. 8 アプリの画面更新 DB API Server Client response ● Serverからデータを取得して表示する画面 fetch

Slide 9

Slide 9 text

Cluster, Inc. All Rights Reserved. 9 アプリの画面更新 DB API Server Client response Update ● Serverからデータを取得して表示する画面 fetch

Slide 10

Slide 10 text

Cluster, Inc. All Rights Reserved. 10 アプリの画面更新 DB API Server Client 反映したい response Update ● Serverからデータを取得して表示する画面 fetch

Slide 11

Slide 11 text

Cluster, Inc. All Rights Reserved. 11 画面更新のパターン ● ユーザーの明示的なアクション ○ PullToRefreshや更新ボタン ● 自動的な更新 ○ serverの更新を検知してリアルタイムな更新処理

Slide 12

Slide 12 text

Cluster, Inc. All Rights Reserved. 12 画面更新のパターン ● ユーザーの明示的なアクション ○ PullToRefreshや更新ボタン ● 自動的な更新 ○ serverの更新を検知してリアルタイムな更新処理

Slide 13

Slide 13 text

Cluster, Inc. All Rights Reserved. 13 ※リアルタイムの定義 ● 本セッションでの ”リアルタイム”は 基本的に soft real-timeの事を意味します ※ soft real-time: 時間内に処理できなくても品質は下がるが価値は0にならない

Slide 14

Slide 14 text

Cluster, Inc. All Rights Reserved. 14 リアルタイム更新が必要な場合 ● ユーザーに情報の更新をいち早く伝えたい場合 ○ ex. チャット, ユーザーのオンライン状態 ● 逆にリアルタイム更新すべきでないケースもある ○ ex. 商品詳細の説明文など

Slide 15

Slide 15 text

15 リアルタイム更新の実装例

Slide 16

Slide 16 text

Cluster, Inc. All Rights Reserved. 16 更新通知の受信方法 1. polling 2. Push通知 3. Firebase Realtime Database / Cloud Firestore 4. WebSocket

Slide 17

Slide 17 text

Cluster, Inc. All Rights Reserved. 17 更新通知の受信方法 1. polling 2. Push通知 3. Firebase Realtime Database / Cloud Firestore 4. WebSocket

Slide 18

Slide 18 text

Cluster, Inc. All Rights Reserved. 18 更新方法1. polling let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in Task { // 差分があれば画面更新 let data = await self.apiClient.fetch() refreshIfNeeded(data) } }

Slide 19

Slide 19 text

Cluster, Inc. All Rights Reserved. 19 更新方法1. polling ● メリット ○ 実装がシンプルでAPIも使いまわせる ● デメリット ○ 無駄なAPI通信が発生するのでパフォーマンスが悪い

Slide 20

Slide 20 text

Cluster, Inc. All Rights Reserved. 20 更新通知の受信方法 1. polling 2. Push通知 3. Firebase Realtime Database / Cloud Firestore 4. WebSocket

Slide 21

Slide 21 text

Cluster, Inc. All Rights Reserved. 21 更新方法2. サイレントPush通知 https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_serv er/pushing_background_updates_to_your_app

Slide 22

Slide 22 text

Cluster, Inc. All Rights Reserved. 22 更新方法2. サイレントPush通知 ● Push通知のpayloadにcontent-available: 1を指定 { "aps": { "content-available": 1 }, "data": { "example":"sync-notify" } }

Slide 23

Slide 23 text

Cluster, Inc. All Rights Reserved. 23 更新方法2. サイレントPush通知 { "aps": { "content-available": 1 }, "data": { "example":"sync-notify" } } ● Push通知のpayloadにcontent-available: 1を指定 任意の値を入れれる

Slide 24

Slide 24 text

Cluster, Inc. All Rights Reserved. 24 更新方法2. サイレントPush通知 func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // サイレントpush受信 // ここで画面に受信通知をする (e.g. NotificationCenter) } ● Appdelegateで受信時の処理をハンドリング

Slide 25

Slide 25 text

Cluster, Inc. All Rights Reserved. 25 更新方法2. サイレントPush通知 ● メリット ○ 既存のPushの仕組みを使いまわせる ● デメリット ○ 他Platformへの流用が難しい ○ ユーザー設定でOFFにできてしまう ○ Push通知を対応していない場合は対応が必要

Slide 26

Slide 26 text

Cluster, Inc. All Rights Reserved. 26 更新通知の受信方法 1. polling 2. Push通知 3. Firebase Realtime Database / Cloud Firestore 4. WebSocket

Slide 27

Slide 27 text

Cluster, Inc. All Rights Reserved. 27 更新方法3. Firebase Realtime Database / Cloud Firestore

Slide 28

Slide 28 text

Cluster, Inc. All Rights Reserved. 28 import FirebaseDatabase ... // Realtime Database let ref = Database.database().reference() ref.child(“users”).observe(.value, with: { snapshot in let value = snapshot.value // 画面更新 refresh(value) }) 更新方法3. Firebase Realtime Database / Cloud Firestore

Slide 29

Slide 29 text

Cluster, Inc. All Rights Reserved. 29 ● メリット ○ client実装がシンプル ○ インフラのメンテナンスコスト低い ■ スケールなどをFirebaseに頼れる ● デメリット ○ FirebaseにDBが依存 更新方法3. Firebase Realtime Database / Cloud Firestore

Slide 30

Slide 30 text

Cluster, Inc. All Rights Reserved. 30 更新通知の受信方法 1. polling 2. Push通知 3. Firebase Realtime Database / Cloud Firestore 4. WebSocket

Slide 31

Slide 31 text

Cluster, Inc. All Rights Reserved. 31 更新方法4. WebSocket Client Server Update通知 Connection Close

Slide 32

Slide 32 text

Cluster, Inc. All Rights Reserved. 32 更新方法4. WebSocket class WebSocketEngine: NSObject, URLSessionWebSocketDelegate { private var task: URLSessionWebSocketTask? func connect() { let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) task = session.webSocketTask(with: .init(string: "ws://example")!) doReceive() task?.resume() } func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { // connected } }

Slide 33

Slide 33 text

Cluster, Inc. All Rights Reserved. 33 更新方法4. WebSocket private func doReceive() { task?.receive { [weak self] (result) in switch result { case .success(let message): switch message { case .string(let string): // Receive string case .data(let data): // Receive data @unknown default: // error } self?.doReceive() case .failure(let error): // error } } }

Slide 34

Slide 34 text

Cluster, Inc. All Rights Reserved. 34 更新方法4. WebSocket func disconnect() { task?.cancel(with: .normalClosure, reason: nil) } func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { // disconnected }

Slide 35

Slide 35 text

Cluster, Inc. All Rights Reserved. 35 更新方法4. WebSocket SwiftのWebSocket library ● Starscream ○ https://github.com/daltoniam/Starscream ● socket.io-client-swift ○ https://github.com/socketio/socket.io-client-swift ● SwiftWebSocket ○ https://github.com/tidwall/SwiftWebSocket

Slide 36

Slide 36 text

Cluster, Inc. All Rights Reserved. 36 更新方法4. WebSocket ● メリット ○ 拡張性/自由度が高い ■ 他Platformでも同様の仕組みが使いまわせる ● デメリット ○ client/server共に実装が大変になりがち

Slide 37

Slide 37 text

Cluster, Inc. All Rights Reserved. 37 各手法のpros / cons比較 メリット デメリット polling シンプルな実装 パフォーマンスが良くない silent push Pushの実装を使いまわせる 拡張性が低い Firebase SDKを使った実装がシンプル mBaasの基盤で安定 Firebaseが使えない環境だと利用で きない WebSocket 自由度 / 拡張性が高い 実装やメンテナンスコストが高い ● 要件 / 仕様と併せて最適なものを選ぶと良い

Slide 38

Slide 38 text

38 clusterでの実例

Slide 39

Slide 39 text

Cluster, Inc. All Rights Reserved. 39 clusterでの実例 ● メッセージや フレンドリクエストに利用

Slide 40

Slide 40 text

Cluster, Inc. All Rights Reserved. 40 clusterでの実例 ● メッセージや フレンドリクエストに利用

Slide 41

Slide 41 text

Cluster, Inc. All Rights Reserved. 41 更新通知はWebSocketを採用 ● 多くのPlatformをカバーできる ○ iOS / Android / Windows / Mac / Quest2 ● inroom(Unity)内の通知にもWebSocketを利用 ● FirebaseのDBを利用していない

Slide 42

Slide 42 text

Cluster, Inc. All Rights Reserved. 42 接続 / 切断の管理 RealtimeSync Manager Auth API WebSocket token

Slide 43

Slide 43 text

Cluster, Inc. All Rights Reserved. 43 接続 / 切断の管理 RealtimeSync Manager Auth API WebSocket token connect(token)

Slide 44

Slide 44 text

Cluster, Inc. All Rights Reserved. 44 接続 / 切断の管理 RealtimeSync Manager Auth API WebSocket token connect(token) 以下の状態をRealtimeSyncManagerが管理 状態に変化がある度に接続(or 切断)処理を行う ・Login状態 ・inroom / outroom ・Foreground / Background ・エラーなどでの切断

Slide 45

Slide 45 text

Cluster, Inc. All Rights Reserved. 45 更新通知の受信 RealtimeSync Manager WebSocket Protocol Buffers Revisions

Slide 46

Slide 46 text

Cluster, Inc. All Rights Reserved. 46 更新通知の受信 RealtimeSync Manager WebSocket Protocol Buffers Revisions 初回接続時 or データ更 新時に送られてくる struct Revisions { var messageRevision: UInt64 var friendRevision: UInt64 var fooRevision: UInt64 var barRevision: UInt64 }

Slide 47

Slide 47 text

Cluster, Inc. All Rights Reserved. 47 更新通知の受信 RealtimeSync Manager WebSocket Protocol Buffers Revisions final class RealtimeSyncManager { var revisions: Observable { revisionsRelay.asObservable() } private var revisionsRelay = PublishRelay() // WebSocketからデータ受信毎に呼ぶ private func doReceive(_ data: Data) { // protobufをdecode guard let revisions = try? Revisions(contiguousBytes: data) else { return } revisionsRelay.accept(revisions) } }

Slide 48

Slide 48 text

Cluster, Inc. All Rights Reserved. 48 更新通知の受信 RealtimeSync Manager WebSocket FriendSync subscribe Protocol Buffers Revisions final class FriendSync { private var currentRevision: UInt64 private let bag = DisposeBag() init(realtimeSyncManager: RealtimeSyncManager, repository: FriendRepository) { realtimeSyncManager.revisions .subscribe(onNext: { [weak self] revisions in guard let self = self else { return } if self.currentRevision != revisions.friendRevision { self.currentRevision = revisions.friendRevision repository.fetchRequest() } }) .disposed(by: bag) } } Revisionの 更新チェック

Slide 49

Slide 49 text

Cluster, Inc. All Rights Reserved. 49 更新通知の受信 RealtimeSync Manager WebSocket FriendSync subscribe Revisionの 更新チェック Protocol Buffers Revisions FriendAPI Friend Repository fetch Friend ViewModel FriendView subscribe data bind 更新をrequest

Slide 50

Slide 50 text

Cluster, Inc. All Rights Reserved. 50 更新通知の受信 RealtimeSync Manager WebSocket FriendSync subscribe Revisionの 更新チェック Protocol Buffers Revisions FriendAPI Friend Repository fetch Friend ViewModel FriendView subscribe data bind 更新をrequest 各画面共通の設計

Slide 51

Slide 51 text

Cluster, Inc. All Rights Reserved. 51 更新通知の受信 RealtimeSync Manager WebSocket Sync subscribe Revisionの 更新チェック Protocol Buffers Revisions API Repository fetch ViewModel View subscribe data bind 更新をrequest 各画面共通の設計 新規でリアルタイム更新を追 加する場合この部分を新設す るだけでOK

Slide 52

Slide 52 text

52 UI更新時に考慮するポイント

Slide 53

Slide 53 text

Cluster, Inc. All Rights Reserved. 53 UIをリアルタイム更新するときに大事な事 ● ユーザーの邪魔しない ○ ユーザーが何かを見ている途中でのインジケーター ○ エラー表示 ○ スクロールしていた画面が勝手に戻る

Slide 54

Slide 54 text

Cluster, Inc. All Rights Reserved. 54 差分更新を使う ● DiffableDataSources var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.section]) snapshot.appendItems(items) dataSource.apply(snapshot)

Slide 55

Slide 55 text

Cluster, Inc. All Rights Reserved. 55 差分更新を使う https://developer.apple.com/videos/play/wwdc2020/10045/

Slide 56

Slide 56 text

56 まとめ

Slide 57

Slide 57 text

Cluster, Inc. All Rights Reserved. 57 まとめ ● 情報をいち早く伝える必要がある場合にリアルタイム更新処理は必要 ○ ex. チャットなど ● 更新通知の受信方法 ○ polling, push通知, Firebase, WebSocket ● 対応画面を柔軟に増やせる更新通知の設計 ● UI更新時にはユーザーの操作を邪魔しない

Slide 58

Slide 58 text

58 最後に

Slide 59

Slide 59 text

Cluster, Inc. All Rights Reserved. 59 絶賛採用強化中! ● iOS Engineerはじめ各職種採用強化中です! https://recruit.cluster.mu/engineer/