リアルタイムリスナーってすごい機能なのにあまり使い道を見出せず単一リクエストばかりしていたのですが、ちょっと使えそうな余地のある部分を発見したので試してみました。
通知ドット
SNSやお知らせなどのタブ上にあるアレです。不定期更新な機能でも見て欲しい時に見てもらうための導線になるのでよいものです。
未読がないか毎回フェッチするよりも、リアルタイムアップデートしたほうが面白いのでリアルタイムにデータを同期して通知ドットを表示させてみようと思います🐞
仕様
- お知らせ一覧へ遷移するタブをもつアプリ
- お知らせに未読があればお知らせタブ上に通知ドットを表示してあげる
- お知らせのソートは常に最新順
実装方法
未読判定
未読かどうかの判定は取得された最新1件のドキュメントIDと既読とみなされたローカルに保存されているドキュメントIDで判断するシンプルなものです。
public struct UnreadState {
private(set) var latestLocalId: String?
private(set) var latestRemoteId: String?
public var hasUnread: Bool {
guard let local = latestLocalId,
let remote = latestRemoteId else {
return false
}
return local != remote
}
// ...
}
お知らせのコレクションをつくる
notifications/{notificationId}
Firestoreリアルタイムリスナーでデータを監視
お知らせコレクションの最新1件をリッスンする
Firestore.firestore()
.collection("notifications")
.order(by: "createdAt", descending: true)
.limit(to: 1)
.addSnapshotListener({ (snapshot, _) in
// 最新1件目のドキュメントID
let latestRemoteId = snapshot?.documents.first?.documentID
})
}
ローカルの未読は とりあえず UserDefaults
お知らせ一覧へ遷移した時に最新の1件のドキュメントIDを既読とみなして保存する。
// お知らせ一覧を取得してきた想定
let notificationId = notifications.first?.id
userDefaults.set(notificationId, forKey: .lastReadNotificationId)
お知らせ一覧へ遷移時に単一リクエストで取得した最新1件を保存する。
このようにフラットに保存してしまうと複数アカウントの未読を管理できないですがここでは知らなかったことにする。
Viewの更新
// 通知ドットView
let notificationDot: DotView = .init()
// state is Observable<UnreadState>
state
.map { $0.hasUnread }
.distinctUntilChanged()
.bind(to: Binder(self) { me, hasUnread in
me.notificationDot.isHidden = !hasUnread
})
.disposed(by: disposeBag)
RxSwiftを利用していますがなんらかの方法でViewに変更を伝えます。
通知ドットView
仕上げに通知ドットViewにはいい感じの出現アニメーションを実装してあげます。
class DotView: UIView {
init() {
super.init(frame: .init(x: 0, y: 0, width: 5, height: 5))
backgroundColor = SPColor.badge
layer.cornerRadius = bounds.midX
isHidden = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var isHidden: Bool {
didSet {
guard oldValue != self.isHidden else {
return
}
if !self.isHidden {
appearAnimation()
}
}
}
private func appearAnimation() {
self.transform = CGAffineTransform(scaleX: 0, y: 0)
UIView.animate(
withDuration: 0.6,
delay: 0,
options: .curveEaseInOut,
animations: { self.transform = CGAffineTransform.identity }
)
}
}
isHidden
プロパティをオーバーライドして出現時に拡大アニメーションを追加しました。
ドーパミンが分泌されます😇
おわりに
このまま実運用だと並び順の変更や未読数カウントなどによって破綻する未来が見えますが、わりと最小限実装で事足りるケースも少なくないかと思います。この機能を応用して更新フラグに利用したりもできそうです。