FastComments.com

在你的 iOS 應用新增留言


這是 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

The feed system is a separate SDK (FastCommentsFeedSDK) with its own view.

載入與顯示動態

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.loadIfNeeded()
            }
    }
}

動態視圖會自動包含下拉重新整理與無限滾動功能。 在畫面生命週期重新進入時,使用 loadIfNeeded(),以避免既有或還原的動態重設回第 1 頁。

建立貼文

使用 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 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 {}

基於標籤的動態過濾

實作 TagSupplier 協定以依標籤過濾動態貼文:

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

sdk.tagSupplier = TeamTagSupplier()

對於不過濾的全域動態,回傳 nil

儲存與還原動態狀態

在視圖生命週期事件中保留分頁狀態:

let state = sdk.savePaginationState()
// Later...
sdk.restorePaginationState(state)
try? await sdk.loadIfNeeded()

如果你的畫面暫時消失,動態視圖會自動暫停即時更新,並在重新出現時恢復,而不會清除已載入的貼文。僅在確實不再使用該 SDK 實例時才呼叫 sdk.cleanup()

刪除貼文

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

PropertyTypeDescription
commentCountOnServerInt伺服器上的評論總數
newRootCommentCountInt緩衝的新評論(當 showLiveRightAway 為 false 時)
currentUserUserSessionInfo?當前已驗證的使用者
isSiteAdminBool當前使用者是否為網站管理員
isClosedBool評論串是否已關閉
hasBillingIssueBool是否存在帳單問題
isLoadingBool是否有網路請求正在進行
hasMoreBool是否存在更多評論頁面
blockingErrorMessageString?阻礙 UI 正常運作的錯誤
warningMessageString?非阻斷的警告訊息
isDemoBool是否以示範模式執行
commentsVisibleBool評論可見性的切換
toolbarEnabledBool是否顯示格式工具列

FastCommentsFeedSDK Published Properties

PropertyTypeDescription
feedPosts[FeedPost]目前已載入的 Feed 帖文
hasMoreBool是否存在更多頁面
currentUserUserSessionInfo?當前已驗證的使用者
blockingErrorMessageString?阻斷性錯誤訊息
isLoadingBool是否有網路請求正在進行
newPostsCountInt自上次載入以來的新帖數量

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 倉庫 以取得貢獻指南。