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 包管理器将 FastCommentsUI 添加到您的项目。

在 Xcode 中:File > Add Package Dependencies,然后输入仓库 URL。

或者将其添加到您的 Package.swift

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

然后将该产品添加到您的目标:

.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. Anonymous -- 无 SSO 令牌;用户获得基于会话的身份
  2. Simple SSO -- 客户端令牌,用于演示和测试(不安全)
  3. Secure SSO -- 服务器签名的令牌,用于生产环境

Simple SSO

适用于演示和本地测试。任何人都可以使用 Simple 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 也支持可选字段:

  • id -- user ID(如果未设置则默认为 email)
  • displayName -- 单独的显示名称
  • displayLabel -- 在姓名旁显示的自定义标签(例如 "VIP")
  • websiteUrl -- 用户姓名上的链接
  • locale -- 区域代码
  • isProfileActivityPrivate -- 隐藏个人资料活动(默认值为 true)

Secure 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()
// 通过你的 API 将此令牌返回给你的 iOS 应用

在你的 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 {
            // 从你的后端获取令牌
            let token = try? await fetchSSOTokenFromYourBackend()
            // 使用令牌创建新的配置,或在加载前设置它
            isLoadingToken = false
            try? await sdk.load()
        }
    }
}

SecureSSOUserData 支持额外字段:

  • optedInNotifications -- email 通知的选择加入
  • 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)
样式外观
._0Up/down arrow buttons with net count
._1Single heart button with count

事件回调

使用修饰器样式的回调来处理用户交互:

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  // Newest first (default)
sdk.defaultSortDirection = .of  // Oldest first
sdk.defaultSortDirection = .mr  // Most relevant


实时聊天 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.

加载与显示 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.loadIfNeeded()
            }
    }
}

Feed 视图会自动包含下拉刷新和无限滚动。
在屏幕生命周期重新进入时使用 loadIfNeeded(),以便已有或恢复的 Feed 不会重置回第 1 页。

创建帖子

使用 FeedPostCreateView 来展示发帖表单:

@State private var showCreatePost = false

// 在你的视图主体中:
.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
        // 处理用户点击
    })
}

注意:对于 .sheet(item:)FeedPost 必须遵循 Identifiable。添加如下扩展:

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)
try? await sdk.loadIfNeeded()

如果你的屏幕短暂消失,Feed 视图会自动暂停实时更新,并在重新出现时恢复,而不会清除已加载的帖子。仅在真正不再使用该 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 事件。处理以下事件:

  • 新的评论、编辑和删除
  • 投票(新增和移除)
  • 置顶、锁定、标记和屏蔽状态更改
  • 用户在线状态(加入/离开)
  • 线程打开/关闭
  • 徽章授予
  • 服务器配置更新

控制实时显示

默认情况下,来自其他用户的新评论会立即出现:

sdk.showLiveRightAway = true   // 默认:立即显示

将此设置为 false 会将新评论缓存在一个 “N 条新评论” 按钮后面,让用户选择何时显示它们:

sdk.showLiveRightAway = false

用户在线状态

当服务器启用存在跟踪时,用户头像上会自动显示在线/离线指示器。客户端无需额外配置。



分页 Internal Link

页面大小

// 评论:默认 30
sdk.pageSize = 50

// 信息流:默认 10
feedSDK.pageSize = 20

加载更多评论

UI 会自动显示分页控件。你也可以以编程方式触发分页:

// 加载下一页
try await sdk.loadMore()

// 加载所有剩余(若评论超过2000则为性能原因被禁用)
try await sdk.loadAll()

// 检查状态
sdk.hasMore            // 是否存在更多页面
sdk.shouldShowLoadAll()
sdk.getCountRemainingToShow()

子评论分页

嵌套回复采用惰性加载。当用户展开一个线程时,会加载前 5 条子评论。如果存在更多,会出现“加载更多回复”控件。此功能由 UI 自动处理。

状态与可观测性 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 已发布的属性

PropertyTypeDescription
commentCountOnServerInt服务器上的评论总数
newRootCommentCountInt缓冲的新根评论数(当 showLiveRightAway 为 false 时)
currentUserUserSessionInfo?当前已认证用户
isSiteAdminBool当前用户是否为站点管理员
isClosedBool评论线程是否已关闭
hasBillingIssueBool是否存在计费问题
isLoadingBool是否有网络请求正在进行
hasMoreBool是否存在更多评论页
blockingErrorMessageString?阻止 UI 正常工作的错误
warningMessageString?非阻塞的警告信息
isDemoBool是否处于演示模式
commentsVisibleBool评论可见性的切换
toolbarEnabledBool是否显示格式化工具栏

FastCommentsFeedSDK 已发布的属性

PropertyTypeDescription
feedPosts[FeedPost]当前加载的 Feed 帖子
hasMoreBool是否存在更多页
currentUserUserSessionInfo?当前已认证用户
blockingErrorMessageString?阻塞性错误信息
isLoadingBool是否有网络请求正在进行
newPostsCountInt自上次加载以来的新帖子数量

评论树

可以通过 sdk.commentsTree 访问评论树:

// Flat list of visible nodes for rendering
sdk.commentsTree.visibleNodes

// Lookup a comment by ID
sdk.commentsTree.commentsById["comment-id"]


欧盟区域 Internal Link


要使用欧盟数据中心,请在配置中设置 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 中调用,或在视图被释放时调用。



图片上传 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 仓库 获取贡献指南。