6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZOZOAdvent Calendar 2024

Day 24

FirestoreだけでSNSでよくあるリアクション機能を実装してみた【SwiftUI】

Last updated at Posted at 2024-12-23

リアクション機能は、「コメント」をするよりも簡単に、「いいね」よりも豊富なフィードバックすることができる優れた機能です。
(Slackでもむやみやたらにリアクションをしてます✌️)

そんなリアクション機能を データベースがFirestore でSwiftUIのアプリでも実現することができたので具体的な実装方法を紹介させていただきたいと思います✊

こんな感じで複数リアクションができます😺

前提条件

  • FirebaseAuthでログインできている
  • FirebaseFirestoreでデータを更新・取得できている

リアクション機能の概要

以下のような機能を兼ねたリアクション機能を実装していきます。

  1. ユーザーは投稿に対して複数種類のリアクション(例: 🥰, 🤣, 🤩)をつけられる
  2. 同じリアクションの重複防止と解除機能
  3. リアクションされたことを投稿者のお知らせする
  4. リアクションするたびにリアルタイムでリアクションを更新
  5. Firestoreのバッチ処理で一貫性のあるデータ操作を行う
  6. Firestoreのキャッシュ機能を活用して通信コストを削減

2. 実装の全体構成

本記事では以下のアーキテクチャに基づいてリアクション機能を実装します!

  1. View (SwiftUI):
    ユーザーインターフェースを構築し、ユーザーからの操作を受け付ける
  2. ViewModel:
    Viewの状態を管理し、操作をUseCaseに委譲する
  3. UseCase:
    Firestoreとキャッシュを操作し、ロジック全体を統括する
  4. Firestore:
    データの保存、更新、削除を管理

IMG_4106.jpg

3. Firestoreのデータ設計

Firestoreでは、以下の3つのコレクションでリアクションを管理します💻

  • Post
  • Reaction
  • Notice

1. Postコレクション

投稿データとリアクションの集計情報を保持します🫡

Firestoreのデータ.json
{
  "id": "post123",
  "reactions": ["smilingHeart", "laughing", "star"],
  "smilingHeartCount": 10,
  "laughingCount": 5,
  "starCount": 3,
  "createdAt": "2024-11-29T12:34:56Z"
}

Swift側のデータ定義

Post.swift
struct Post: Identifiable, Codable, Hashable {
  var id: String
  var reactions: [ReactionType]
  var smillingHeartCount: Int?
  var laughingCount: Int?
  var flushedCount: Int?
  var partyingCount: Int?
  var starCount: Int?
  var thumbsUpCount: Int?
}

2. Reactionサブコレクション

投稿ごとの個別のリアクションを記録します。

{
  "id": "post123_user123_smillingHeart",
  "type": "smillingHeart",
  "postId": "post123",
  "reactionUserId": "user123",
  "posterUserId": "user234",
  "createdAt": "2024-11-29T12:34:56Z"
}

Swift側のデータ定義

Reaction.swift
/// ユーザが投稿したpostに対するリアクション
struct Reaction: Codable, Sendable {
  var id: String
  /// リアクションタイプ
  var type: ReactionType
  /// 投稿ID
  var postId: String
  /// リアクションした人の情報
  var reactionUserId: String
  /// 投稿者の情報
  var posterUserId: String
  var createdAt: Date
}

3. Noticeコレクション

投稿者に通知を送るためのデータを保持します。

{
  "id": "post123_user123_smillingHeart",
  "targetUserId": "user123",
  "message": "user123があなたの投稿に🥰を付けました。",
  "createdAt": "2024-11-29T12:34:56Z"
}
Notice.swift
struct Notice: Codable, Identifiable, Sendable {
  var id: String
  var targetUserId: String
  var message: String
  var createdAt: Date
}

4. 各層の実装詳細

ロジック全体のフロー

以下は、リアクションの追加・削除処理を視覚的に表現したフローチャートです。Firestoreのバッチ処理を活用し、投稿データ・リアクションデータ・通知データを一括で更新または削除します。

animated.gif


1. FirestoreManager: バッチ処理

Firestoreのバッチ処理を利用し、リアクションの追加・削除を安全に行います。
(バッチ処理にすることで、今回書き込む3つの処理が一つでも失敗したら全て失敗にさせることで原子性を保つことができます)

リアクション追加の処理

FirestoreManager.swift
    let batch = firestore.batch()
    
    let postsRef = firestore.collection("posts").document(postId)
    let reactionRef = firestore.collection("reactions").document(reactionId)
    let noticeRef = firestore.collection("users").document(userId).collection("notices").document(noticeId)

    batch.updateData(postDocumentData, forDocument: postsRef)
    batch.setData(reactionDocumentData, forDocument: reactionRef)
    batch.setData(noticeDocumentData, forDocument: noticeRef)

    try await batch.commit()

リアクション削除の処理

FirestoreManager.swift
    let batch = firestore.batch()
    let postsRef = firestore.collection("posts").document(postId)
    let reactionRef = firestore.collection("reactions").document(reactionId)
    let noticeRef = firestore.collection("users").document(userId).collection("notices").document(noticeId)

    batch.updateData(postDocumentData, forDocument: postsRef)
    batch.deleteDocument(reactionRef)
    batch.deleteDocument(noticeRef)

    try await batch.commit()

2. UseCase: ビジネスロジックの統括

リアクションの追加・削除を条件によって分岐させます。

UseCase.swift
func updateReaction(post: Post, mySelf: User, type: ReactionType) async throws -> Reaction? {
    let reactionId = "\(post.id)_\(mySelf.id)_\(type.rawValue)"
    let noticeId = "\(post.id)_\(mySelf.id)_\(type.rawValue)"

    /// リアクションをしているかどうか最新の情報を取得する
    if try await reactionRepository.alreadyReactioned(post: post, reactionId: reactionId) {
        // リアクション削除
        try await firestoreManager.batchDeleteReaction(...)
        return nil
    } else {
        // リアクション追加
        let newReaction = Reaction(...)
        try await firestoreManager.batchAddReaction(...)
        return newReaction
    }
}

3. ViewModel: 状態管理

ViewとUseCaseの橋渡しを行い、リアクションの状態を管理します。

なおView側で既にリアクションした情報を保持しておくために ReactionCacheManagerを作成します。
これはアプリ起動時に自分がしたリアクション最新100件を取得しておくものです。
小れをすることによってViewで都度更新状態を取得する必要がなくなります。

ViewModel.swift
func didTapReaction(type: ReactionType) async {
    guard !showLoading, let user = await userInfoCacheManager.cachedUser else { return }
    defer { showLoading = false }

    do {
        showLoading = true
        let newReaction = try await useCase.updateReaction(post: post, mySelf: user, type: type)
        if let newReaction {
            myReactions.append(newReaction)
        } else {
            myReactions.removeAll(where: { $0.type == type })
        }
    } catch {
        print("Error updating reaction: \(error)")
    }
}

4. SwiftUI: リアクションボタンのUI

Viewにボタンを追加します。Postに含まれる reactionsをForEachで回して表示させるだけです!

View.swift
@ViewBuilder
func reactionButton(type: ReactionType) -> some View {
       Button {
            Task { await viewModel.didTapReaction(type: type) }
        } label: {
            HStack {
            emoji.resizable().frame(width: 16, height: 16)
            Text("\(viewModel.post.reactionCount(for: type))")
        }
    }
}

5. まとめ

今回の記事では、Firestoreのバッチ処理を活用し、SwiftUIを使ったSNSの複数リアクション機能の実装手順を詳しく解説しました。この機能を構築することで、ユーザーは投稿に対して多様な感情を表現でき、投稿者への通知を通じてインタラクションを促進する仕組みを作ることができます。

Firestoreのバッチ処理を用いることで、データ操作の一貫性を保ちながら、リアクション数が増えても効率的に処理を行えます。
またFirestoreだけで完結する設計により、サーバー側の複雑なロジックを省き、メンテナンスコストを抑えることができます。

フィードバックを歓迎します!

もしこの記事を読んで、「こういう部分がもっと知りたい」「ここは改善した方が良い」などのフィードバックがあれば、ぜひコメント欄で教えてください。また、今回の実装に興味を持った方は、ぜひ実際にコードを書いて試してみてください!新しい発見や改善点があれば、それもぜひ共有していただけると嬉しいです。

本記事が、あなたのSNSアプリ開発に役立つことを願っています!
ぜひ、この実装をベースに自分だけのカスタマイズを加え、より魅力的なアプリを作り上げてください。

僕の作った魅力的なアプリも使ってみてね👊

6
0
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
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?