Help us understand the problem. What is going on with this article?

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

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

通知ドット

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

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

おわりに

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

trueSuperior
魔法が好き🧙‍♂️
signplace
ソーシャルマップSNS「SignPlace」を開発・運営するスタートアップ
https://signplace.co
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした