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 包管理器将 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)
样式 外观
._0 Up/down arrow buttons with net count
._1 Single 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 系统是一个独立的 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

// 在你的视图主体中:
.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)

删除帖子

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 已发布的属性

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 已发布的属性

Property Type Description
feedPosts [FeedPost] 当前加载的 Feed 帖子
hasMore Bool 是否存在更多页
currentUser UserSessionInfo? 当前已认证用户
blockingErrorMessage String? 阻塞性错误信息
isLoading Bool 是否有网络请求正在进行
newPostsCount Int 自上次加载以来的新帖子数量

评论树

可以通过 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 仓库 获取贡献指南。