FastComments.com

Add Comments to Your iOS App


這是 FastComments 的官方 iOS 函式庫。

在你的 iOS 應用中嵌入即時評論、聊天和審核小工具。

儲存庫

在 GitHub 上查看


功能 Internal Link

  • 具備巢狀回覆與分頁的多層討論串留言樹
  • 具備貼文創建、回應與媒體附件的社群動態
  • 即時聊天模式,含自動滾動與日期分隔
  • 透過 WebSocket 的即時更新(新留言、投票、使用者在線狀態)
  • 單一登入(測試用的簡易 SSO、上線用的安全 SSO)
  • 富文字編輯,支援粗體、斜體、程式碼與 @提及
  • 支援可配置樣式的投票(上下箭頭或愛心)
  • 管理操作:檢舉、置頂、鎖定、封鎖
  • 完整主題化,提供預設樣式與完整客製化
  • 自訂工具列按鈕,用於留言與動態貼文的建立
  • 圖片上傳
  • 支援歐盟地區
  • 使用者在席狀態(在線/離線指示)
  • 基於標籤的動態過濾
  • 在地化支援

需求 Internal Link


  • iOS 16+ 或 macOS 14+
  • Swift 5.9+
  • SwiftUI

安裝 Internal Link

使用 Swift Package Manager 將 FastCommentsUI 新增到您的專案。

在 Xcode 中:File > Add Package Dependencies,然後輸入儲存庫 URL。

或者將它新增到您的 Package.swift

dependencies: [
    .package(url: "https://github.com/fastcomments/fastcomments-ios.git", from: "1.0.0")
]

接著將該產品加入到您的 target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "FastCommentsUI", package: "fastcomments-ios")
    ]
)

在需要的地方匯入這兩個模組:

import FastCommentsUI
import FastCommentsSwift

快速開始 Internal Link

顯示評論小工具的最小設定:

import SwiftUI
import FastCommentsUI

struct ContentView: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "demo",
            urlId: "my-page-1",
            url: "https://example.com/page-1",
            pageTitle: "My Page"
        )
    )

    var body: some View {
        FastCommentsView(sdk: sdk)
            .task {
                try? await sdk.load()
            }
    }
}

"demo" 替換為您的 FastComments 租戶 ID。urlId 用來識別儲存評論的頁面或討論串。

驗證(SSO) Internal Link

FastComments 支援三種驗證模式:

  1. 匿名 -- 無 SSO 令牌;使用者會獲得基於會話的身分
  2. 簡易 SSO -- 用於展示與本機測試的用戶端令牌(不安全)
  3. 安全 SSO -- 適用於生產環境、由伺服器簽署的令牌

簡易 SSO

適用於展示和本機測試。任何人都可以使用簡易 SSO 偽裝成任何使用者,因此不要在生產環境中使用。

import FastCommentsSwift

let userData = SimpleSSOUserData(
    username: "Jane Doe",
    email: "jane@example.com",
    avatar: "https://example.com/avatar.jpg"
)
let sso = FastCommentsSSO.createSimple(simpleSSOUserData: userData)
let token = try? sso.prepareToSend()

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page-1",
    sso: token
)
let sdk = FastCommentsSDK(config: config)

SimpleSSOUserData also supports optional fields:

  • id -- 使用者 ID(如果未設定則預設為電子郵件)
  • displayName -- 分開的顯示名稱
  • displayLabel -- 顯示在名稱旁的自訂標籤(例如「VIP」)
  • websiteUrl -- 使用者名稱的連結
  • locale -- 語系代碼
  • isProfileActivityPrivate -- 隱藏個人檔案活動(預設為 true)

安全 SSO

在生產環境中,您的後端會使用 API 秘密產生已簽署的 SSO 令牌。iOS 應用程式會從您的伺服器取得該令牌並傳遞至設定中。

在您的後端(使用 FastComments Swift SDK 或任何語言):

let userData = SecureSSOUserData(
    id: "user-123",
    email: "user@example.com",
    username: "Display Name",
    avatar: "https://example.com/avatar.jpg"
)
let sso = try FastCommentsSSO.createSecure(apiKey: "YOUR_API_KEY", secureSSOUserData: userData)
let token = try sso.prepareToSend()
// Return this token to your iOS app via your API

在您的 iOS 應用程式中:

struct MyView: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "my-page-1"
        )
    )
    @State private var isLoadingToken = true

    var body: some View {
        Group {
            if isLoadingToken {
                ProgressView("Loading...")
            } else {
                FastCommentsView(sdk: sdk)
            }
        }
        .task {
            // Fetch the token from your backend
            let token = try? await fetchSSOTokenFromYourBackend()
            // Create a new config with the token, or set it before load
            isLoadingToken = false
            try? await sdk.load()
        }
    }
}

SecureSSOUserData supports additional fields:

  • optedInNotifications -- 選擇接收電子郵件通知
  • displayLabel -- 自訂標籤
  • displayName -- 顯示名稱
  • websiteUrl -- 網站 URL
  • groupIds -- 所屬群組
  • isAdmin -- 管理員權限
  • isModerator -- 版主權限
  • isProfileActivityPrivate -- 個人檔案活動隱私


分層留言 Internal Link

基本用法

struct CommentsPage: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "article-42",
            url: "https://example.com/article/42",
            pageTitle: "Article Title"
        )
    )

    var body: some View {
        FastCommentsView(sdk: sdk)
            .task {
                try? await sdk.load()
            }
    }
}

投票樣式

預設的投票樣式顯示上下箭頭。傳入 ._1 可使用愛心樣式的投票:

FastCommentsView(sdk: sdk, voteStyle: ._1)
樣式 外觀
._0 上下箭頭按鈕,顯示淨票數
._1 單一愛心按鈕,並顯示數量

事件回調

使用 modifier 風格的回調來處理使用者互動:

FastCommentsView(sdk: sdk)
    .onCommentPosted { comment in
        print("New comment: \(comment.commentHTML)")
    }
    .onReplyClick { renderableComment in
        print("Replying to: \(renderableComment.comment.id)")
    }
    .onUserClick { context, userInfo, source in
        // source 是 .name 或 .avatar
        print("Tapped \(userInfo.displayName)")
    }

套用主題

透過 SwiftUI 環境傳入主題:

FastCommentsView(sdk: sdk)
    .fastCommentsTheme(myTheme)
    .task { try? await sdk.load() }

或直接在 SDK 上設定:

sdk.theme = FastCommentsTheme.modern

排序方向

sdk.defaultSortDirection = .nf  // 由新到舊(預設)
sdk.defaultSortDirection = .of  // 由舊到新
sdk.defaultSortDirection = .mr  // 最相關

即時聊天 Internal Link

LiveChatView 提供即時聊天體驗,具自動滾動、日期分隔符和緊湊的佈局。它會自動將 SDK 設定為由最舊至最新排序並立即顯示實況。

struct ChatView: View {
    @StateObject private var sdk: FastCommentsSDK = {
        let config = FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "chat-room-1",
            sso: ssoToken  // 建議使用 SSO,讓使用者有名稱
        )
        return FastCommentsSDK(config: config)
    }()

    var body: some View {
        LiveChatView(sdk: sdk)
            .onCommentPosted { comment in
                print("Sent: \(comment.commentHTML)")
            }
            .task {
                try? await sdk.load()
            }
    }
}

LiveChatView 支援以下回呼:

  • .onCommentPosted -- 在使用者發送訊息時觸發
  • .onCommentDeleted -- 當訊息被刪除時觸發
  • .onUserClick -- 當使用者的名稱或頭像被點擊時觸發


社群動態 Internal Link

Feed 系統是獨立的 SDK(FastCommentsFeedSDK),並具有自己的檢視。

載入並顯示 Feed

struct FeedPage: View {
    @StateObject private var sdk: FastCommentsFeedSDK = {
        let config = FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "my-feed",
            sso: ssoToken
        )
        return FastCommentsFeedSDK(config: config)
    }()

    @State private var commentsPost: FeedPost?

    var body: some View {
        FastCommentsFeedView(sdk: sdk)
            .onPostSelected { post in
                commentsPost = post
            }
            .onCommentsRequested { post in
                commentsPost = post
            }
            .onSharePost { post in
                // 顯示分享畫面
            }
            .onUserClick { context, userInfo, source in
                // 導覽至使用者檔案
            }
            .onMediaClick { mediaItem, index in
                // 顯示全螢幕圖像檢視器
            }
            .task {
                try? await sdk.load()
            }
    }
}

Feed 檢視會自動包含下拉重新整理與無限滾動。

建立文章

使用 FeedPostCreateView 顯示文章建立表單:

@State private var showCreatePost = false

// In your view body:
.sheet(isPresented: $showCreatePost) {
    FeedPostCreateView(
        sdk: sdk,
        onPostCreated: { post in
            showCreatePost = false
            Task { try? await sdk.refresh() }
        },
        onCancelled: {
            showCreatePost = false
        }
    )
}

對文章做出反應

SDK 使用樂觀更新來處理反應:

try await sdk.reactPost(postId: post.id, reactionType: "l")

// Check reaction state
let hasLiked = sdk.hasUserReacted(postId: post.id, reactType: "l")
let likeCount = sdk.getLikeCount(postId: post.id)

在文章上開啟留言

使用 CommentsSheet 顯示 Feed 文章的留言。它會使用 Feed SDK 的設定在內部建立一個 FastCommentsSDK 實例:

.sheet(item: $commentsPost) { post in
    CommentsSheet(post: post, feedSDK: sdk, onUserClick: { context, userInfo, source in
        // 處理使用者點擊
    })
}

注意:FeedPost 必須遵從 Identifiable,以便使用 .sheet(item:)。新增此延伸:

extension FeedPost: @retroactive Identifiable {}

根據標籤篩選 Feed

實作 TagSupplier 協定以依標籤篩選 Feed 文章:

struct TeamTagSupplier: TagSupplier {
    func getTags(currentUser: UserSessionInfo?) -> [String]? {
        guard let user = currentUser else { return nil }
        return ["team:\(user.id ?? "")", "public"]
    }
}

sdk.tagSupplier = TeamTagSupplier()

對於未篩選的全域 Feed,回傳 nil

儲存與還原 Feed 狀態

在檢視生命週期事件之間保存分頁狀態:

let state = sdk.savePaginationState()
// Later...
sdk.restorePaginationState(state)

刪除文章

sdk.onPostDeleted = { postId in
    print("Post \(postId) was deleted")
}

主題設定 Internal Link

主題預設

提供四個內建預設:

// 系統預設
sdk.theme = FastCommentsTheme.default

// 有陰影且大圓角的卡片
sdk.theme = FastCommentsTheme.modern

// 扁平,無陰影,小圓角,無討論串線
sdk.theme = FastCommentsTheme.minimal

// 將所有操作顏色設定為單一品牌色
sdk.theme = FastCommentsTheme.allPrimary(.indigo)

評論顯示樣式

var theme = FastCommentsTheme()
theme.commentStyle = .flat    // 扁平列表,含分隔線(預設)
theme.commentStyle = .card    // 帶陰影的圓角卡片
theme.commentStyle = .bubble  // 聊天泡泡樣式

顏色

所有顏色屬性都是選填。未設定的值會回退到合適的系統預設。

var theme = FastCommentsTheme()

// 品牌色
theme.primaryColor = .indigo
theme.primaryLightColor = .indigo.opacity(0.6)
theme.primaryDarkColor = Color(red: 0.2, green: 0.1, blue: 0.5)

// 背景
theme.commentBackgroundColor = Color(.secondarySystemGroupedBackground)
theme.containerBackgroundColor = Color(.systemGroupedBackground)

// 操作按鈕
theme.actionButtonColor = .indigo
theme.replyButtonColor = .indigo
theme.toggleRepliesButtonColor = .indigo.opacity(0.8)
theme.loadMoreButtonTextColor = .indigo

// 投票
theme.voteActiveColor = .red
theme.voteCountColor = .primary
theme.voteCountZeroColor = .secondary
theme.voteDividerColor = Color(.separator)

// 連結
theme.linkColor = .indigo
theme.linkColorPressed = .indigo.opacity(0.5)

// 對話框
theme.dialogHeaderBackgroundColor = .indigo
theme.dialogHeaderTextColor = .white

// 輸入列
theme.inputBarBackgroundColor = Color(.systemBackground)
theme.inputBarBorderColor = Color(.separator)

// 其他
theme.onlineIndicatorColor = .green
theme.separatorColor = Color(.separator)
theme.badgeBackgroundColor = .gray.opacity(0.2)
theme.threadLineColor = .indigo.opacity(0.15)

排版

theme.commenterNameFont = .subheadline.weight(.bold)
theme.bodyFont = .body
theme.captionFont = .caption
theme.actionFont = .caption.weight(.medium)

佈局與間距

theme.cornerRadius = .large       // 可選:.none, .small, .medium, .large
theme.commentSpacing = 4          // 評論列之間的間距(單位:點)
theme.nestingIndent = 20          // 每層巢狀的縮排(單位:點)
theme.avatarSize = 36             // 根評論的頭像直徑
theme.replyAvatarSize = 28        // 巢狀回覆的頭像直徑

視覺效果

theme.showShadows = true          // 卡片上的細微陰影
theme.showThreadLine = true       // 連接巢狀回覆的垂直線
theme.animateVotes = true         // 投票變化時的彈簧動畫

套用主題

兩種方式:

// 透過 SwiftUI 環境(建議用於檢視層級)
FastCommentsView(sdk: sdk)
    .fastCommentsTheme(theme)

// 直接在 SDK 上設定
sdk.theme = theme


自訂工具列按鈕 Internal Link

評論工具列按鈕

實作 CustomToolbarButton 協定,以新增按鈕到評論輸入工具列:

struct EmojiButton: CustomToolbarButton {
    let id = "emoji"
    let iconSystemName = "face.smiling"       // SF Symbol 名稱
    let contentDescription = "Add Emoji"
    let badgeText: String? = nil              // 可選的徽章數量

    func onClick(text: Binding<String>) {
        text.wrappedValue += "\u{1F44D}"
    }

    // 可選的覆寫(預設為 true)
    func isEnabled() -> Bool { true }
    func isVisible() -> Bool { true }
}

在建立視圖時傳入自訂按鈕:

FastCommentsView(
    sdk: sdk,
    customToolbarButtons: [EmojiButton(), CodeBlockButton()]
)

或在 SDK 上全域新增(套用到所有實例):

sdk.addGlobalCustomToolbarButton(EmojiButton())
sdk.removeGlobalCustomToolbarButton(id: "emoji")
sdk.clearGlobalCustomToolbarButtons()

Feed 工具列按鈕

針對貼文建立表單,實作 FeedCustomToolbarButton

struct HashtagButton: FeedCustomToolbarButton {
    let id = "hashtag"
    let iconSystemName = "number"
    let contentDescription = "Add Hashtag"

    func onClick(content: Binding<String>) {
        content.wrappedValue += "#"
    }
}

傳遞它們到建立視圖:

FeedPostCreateView(
    sdk: sdk,
    customToolbarButtons: [HashtagButton()],
    onPostCreated: { _ in },
    onCancelled: { }
)

或在 feed SDK 上全域設定:

sdk.globalFeedToolbarButtons = [HashtagButton()]


內容審核 Internal Link

所有使用者可用的操作

  • 檢舉/取消檢舉 -- 回報評論以供審查
try await sdk.flagComment(commentId: commentId)
try await sdk.unflagComment(commentId: commentId)
  • 封鎖/取消封鎖 -- 隱藏該使用者的所有評論(以檢視者為單位)
try await sdk.blockUser(commentId: commentId)
try await sdk.unblockUser(commentId: commentId)

僅限管理員的操作

  • 置頂/取消置頂 -- 將評論釘選於討論串頂端
try await sdk.pinComment(commentId: commentId)
try await sdk.unpinComment(commentId: commentId)
  • 鎖定/解鎖 -- 防止在該評論上新增回覆
try await sdk.lockComment(commentId: commentId)
try await sdk.unlockComment(commentId: commentId)

所有管理操作也可以透過 UI 中的評論快顯選單執行。僅當目前使用者為站點管理員(透過 SSO isAdmin 標誌或在儀表板設定)時,才會顯示管理員操作。



即時更新 Internal Link


在呼叫 sdk.load() 之後,SDK 會自動訂閱為已設定 urlId 所開啟的 WebSocket 事件。處理以下事件:

  • 新的留言、編輯與刪除
  • 投票(新增與移除)
  • 釘選、鎖定、檢舉與封鎖狀態變更
  • 使用者在席(加入/離開)
  • 討論串開啟/關閉
  • 徽章授予
  • 伺服器設定更新

Controlling Live Display

預設情況下,其他使用者的新留言會立即顯示:

sdk.showLiveRightAway = true   // 預設:立即顯示

將此設為 false,可將新留言暫存於 "N 則新留言" 按鈕之後,讓使用者選擇何時顯示它們:

sdk.showLiveRightAway = false

使用者在席

當伺服器啟用出席追蹤時,使用者頭像上會自動顯示在線/離線指示。不需要在客戶端進行額外設定。



分頁 Internal Link

頁面大小

// 評論:預設 30
sdk.pageSize = 50

// Feed:預設 10
feedSDK.pageSize = 20

載入更多評論

使用者介面會自動顯示分頁控制項。您也可以以程式觸發分頁:

// 載入下一頁
try await sdk.loadMore()

// 載入所有剩餘(若超過 2000 則為效能考量而停用)
try await sdk.loadAll()

// 檢查狀態
sdk.hasMore            // 是否還有更多頁面
sdk.shouldShowLoadAll()
sdk.getCountRemainingToShow()

子評論分頁

巢狀回覆採延遲載入。當使用者展開討論串時,會載入前 5 個子回覆。如果還有更多回覆,會顯示「載入更多回覆」控制項。此行為由使用者介面自動處理。



狀態與可觀察性 Internal Link


Both FastCommentsSDK and FastCommentsFeedSDK are ObservableObject classes with @Published properties. You can observe these in your SwiftUI views for reactive UI updates.

FastCommentsSDK Published Properties

Property Type Description
commentCountOnServer Int 伺服器上的評論總數
newRootCommentCount Int 緩衝的新評論(當 showLiveRightAway 為 false 時)
currentUser UserSessionInfo? 當前已驗證的使用者
isSiteAdmin Bool 當前使用者是否為網站管理員
isClosed Bool 評論串是否已關閉
hasBillingIssue Bool 是否存在帳單問題
isLoading Bool 是否有網路請求正在進行
hasMore Bool 是否存在更多評論頁面
blockingErrorMessage String? 阻礙 UI 正常運作的錯誤
warningMessage String? 非阻斷的警告訊息
isDemo Bool 是否以示範模式執行
commentsVisible Bool 評論可見性的切換
toolbarEnabled Bool 是否顯示格式工具列

FastCommentsFeedSDK Published Properties

Property Type Description
feedPosts [FeedPost] 目前已載入的 Feed 帖文
hasMore Bool 是否存在更多頁面
currentUser UserSessionInfo? 當前已驗證的使用者
blockingErrorMessage String? 阻斷性錯誤訊息
isLoading Bool 是否有網路請求正在進行
newPostsCount Int 自上次載入以來的新帖數量

Comment Tree

The comment tree is accessible via sdk.commentsTree:

// 用於渲染的可見節點扁平列表
sdk.commentsTree.visibleNodes

// 透過 ID 查找評論
sdk.commentsTree.commentsById["comment-id"]


歐盟區域 Internal Link


要使用歐盟 (EU) 資料中心,請在您的設定中將 region 欄位設為:

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page",
    region: "eu"
)

這會將所有 API 請求和 WebSocket 連線導向 eu.fastcomments.com



清理 Internal Link


當您完成使用 SDK 實例時(例如視圖即將被關閉),呼叫 cleanup() 以關閉 WebSocket 連線並取消背景任務:

sdk.cleanup()

對於由 SwiftUI 的 @StateObject 管理的視圖,通常會在 .onDisappear 或視圖被釋放(deallocated)時呼叫。



圖片上傳 Internal Link


評論

let imageUrl = try await sdk.uploadImage(imageData: jpegData, filename: "photo.jpg")

回傳已上傳圖片的 URL 字串。

動態貼文

let mediaItem = try await feedSDK.uploadImage(imageData: jpegData, filename: "photo.jpg")

// 並行上傳多張圖片
let mediaItems = try await feedSDK.uploadImages(images: [
    (jpegData1, "photo1.jpg"),
    (jpegData2, "photo2.jpg")
])


使用者提及 Internal Link

搜尋使用者以支援 @mention 的自動完成功能:

let results = try await sdk.searchUsers(query: "jan")
// 回傳 [UserSearchResult],包含 userId、username、avatar 等。

內建的 CommentInputBar 會自動處理 @mention 的自動完成功能。



編輯與刪除留言 Internal Link


Edit

try await sdk.editComment(commentId: commentId, newText: "Updated text")

伺服器會重新渲染 HTML。本地留言會自動更新。

Delete

try await sdk.deleteComment(commentId: commentId)

刪除留言也會從本地樹狀結構中移除其子孫。

兩個操作在 UI 的留言快顯選單中可用,當前使用者為留言作者(或網站管理員)時。



錯誤處理 Internal Link

SDK 方法會拋出 FastCommentsError,它符合 LocalizedError

do {
    try await sdk.load()
} catch let error as FastCommentsError {
    print(error.translatedError ?? error.reason ?? "Unknown error")
} catch {
    print(error.localizedDescription)
}

FastCommentsError 屬性:

  • code -- 來自 API 的錯誤代碼
  • reason -- 英文錯誤描述
  • translatedError -- 伺服器提供的本地化錯誤訊息

封鎖錯誤也會自動透過 sdk.blockingErrorMessage 顯示出來,內建視圖會將其展示給使用者。



在地化 Internal Link

在設定中傳入語系代碼以在地化伺服器提供的字串:

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page",
    locale: "fr_fr"
)

用戶端 UI 字串使用基於 iOS bundle 的在地化。



範例應用程式 Internal Link


此儲存庫在 ExampleApp/ 包含一個完整範例應用程式,示範:

  • 具有 SSO 與自訂主題的串狀評論
  • 社群動態,含貼文建立與標籤篩選
  • 即時聊天
  • 簡單且安全的 SSO 流程
  • 自訂工具列按鈕(評論與動態)


需要幫助?

如果您在使用 iOS 函式庫時遇到任何問題或有任何疑問,請:

貢獻

歡迎貢獻!請造訪 GitHub 倉庫 以取得貢獻指南。