14
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Firestoreリアルタイムリスナーで通知ドットを実装してみたよ

Last updated at Posted at 2019-12-19

リアルタイムリスナーってすごい機能なのにあまり使い道を見出せず単一リクエストばかりしていたのですが、ちょっと使えそうな余地のある部分を発見したので試してみました。

通知ドット

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プロパティをオーバーライドして出現時に拡大アニメーションを追加しました。

dotview.gif

ドーパミンが分泌されます😇

おわりに

このまま実運用だと並び順の変更や未読数カウントなどによって破綻する未来が見えますが、わりと最小限実装で事足りるケースも少なくないかと思います。この機能を応用して更新フラグに利用したりもできそうです。

14
14
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
14
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?