# 设置 Pushwoosh InboxKit iOS

*自 iOS SDK [7.0.40](https://github.com/Pushwoosh/pushwoosh-ios-sdk/releases/tag/7.0.40) 起可用。*

Pushwoosh InboxKit 在现有收件箱后端的基础上提供了一个现代化的 UIKit 收件箱屏幕。六种默认的单元格布局涵盖了常见的内容卡片形式——从简单的横幅到图片轮播、内联视频和 Apple Wallet 通行证——内联 CTA 按钮处理最常见的交互，并且整个界面都开放用于子类化，以满足您定制外观的需求。

<img src="/setting-up-pushwoosh-inboxkit-ios-feed.webp" alt="InboxKit 信息流，显示横幅、带标题、经典、轮播、视频和 Apple Wallet 卡片" width="300" style="display: block; margin: 0 auto;"/>

<p style="text-align: center; opacity: 0.7; font-size: 0.875rem; margin-top: 0.5rem;">默认的 InboxKit 信息流，包含横幅、带标题、经典、轮播、视频和 Apple Wallet 卡片。</p>

## 何时使用 InboxKit

对于任何新的 iOS 集成，请使用 InboxKit。它是对旧版 Objective-C [PushwooshInboxUI](/zh/developer/pushwoosh-sdk/ios-sdk/setting-up-pushwoosh-inboxui-ios/) 模块的推荐替代品。

InboxKit 为您提供：

- 六种内置单元格类型——横幅、带标题、经典、轮播、视频和 Apple Wallet——可通过 payload 的 `displayType` 为每条消息选择，或通过代码中的 `attributes.forceCellKind` 强制指定。完整列表请参见[卡片类型](#card-types)。（Apple Wallet 卡片仅适用于 iOS。）
- 带有类型化 `PushwooshInboxButtonAction` 枚举（`openURL`、`dismiss`、`markRead`、`custom`）的内联 CTA 按钮。SDK 会自动处理前三种；您的委托将 `custom` 路由到您自己的逻辑。
- 置顶支持：`actionParams["pinned"] == true` 的消息会浮动到信息流顶部并呈现一个图钉图标。
- 滑动删除、下拉刷新、消失时自动标记为已读——所有这些都可以通过 `PushwooshInboxKitAttributes` 进行切换。
- 持久化存储：即使网络调用尚未被确认，删除和已读状态在进程重启后依然保留。
- 一个开放的 `PushwooshInboxCell` 基类，用于完全自定义的布局。

服务器契约保持不变——与之前一样，使用相同的 Pushwoosh 收件箱后端、payload 和仪表板工具。

## 选择您的集成方法

- [**使用 Swift Package Manager 设置 InboxKit**](/zh/developer/pushwoosh-sdk/ios-sdk/setting-up-pushwoosh-inboxkit-ios/set-up-inboxkit-with-swift-package-manager/) — 推荐用于新项目。
- [**使用 CocoaPods 设置 InboxKit**](/zh/developer/pushwoosh-sdk/ios-sdk/setting-up-pushwoosh-inboxkit-ios/set-up-inboxkit-with-cocoapods/) — 适用于已在使用 CocoaPods 的项目。

## 卡片类型

InboxKit 为每条消息选择一个单元格布局。默认解析器从推送 payload 中读取 `displayType`——将其放在 `data` 对象内，SDK 会在 `actionParams` 下传递该对象。当 `displayType` 缺失时，解析器会回退到一个启发式规则：有图片无标题 → 横幅，有图片有标题 → 带标题，否则为经典。要从代码中为整个信息流强制使用一种布局，请设置 `attributes.forceCellKind`。

每种富文本布局都能优雅地降级：如果所需的 payload 缺失或格式错误，卡片会回退到 `classic`，而不是渲染一个空的占位符（并会记录一个 `WARN`）。

| `displayType` | 布局 | 必需的 payload 字段 | 降级为 |
|---|---|---|---|
| `banner` | 全出血图片，无文本 | 图片（`inbox_image` 或 `data.image`） | 无图片时为 `classic` |
| `captioned` | 顶部图片，下方标题+正文 | 图片（`inbox_image` 或 `data.image`） | 无图片时为 `classic` |
| `classic` | 彩色首字母头像+标题+正文 | — | — |
| `carousel` | 可滑动的多图库 | `data.carousel`（幻灯片数组） | 无幻灯片时为 `classic` |
| `video` | 带播放徽章的海报，点击后全屏播放 | `data.video`（`url` + 可选的 `poster`） | 无描述符时为 `classic` |
| `wallet` | “添加到 Apple Wallet” 按钮（仅限 iOS） | `data.wallet`（`.pkpass` URL） | 无通行证 URL 时为 `classic` |

{/* TODO(screenshot): replace each placeholder below with a per-card screenshot. Suggested filenames: setting-up-pushwoosh-inboxkit-ios-card-banner.webp, -captioned.webp, -classic.webp, -carousel.webp, -video.webp, -wallet.webp — then swap each <div> for an <img src="/<filename>" width="240" .../>. */}
<div style="display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; margin: 1.5rem 0;">
  <figure style="width: 200px; margin: 0;"><img src="/setting-up-pushwoosh-inboxkit-ios-card-banner.webp" alt="InboxKit 横幅卡片" style="width: 100%; border-radius: 12px;"/><figcaption style="text-align: center; font-size: 0.85rem; color: #94a3b8; margin-top: 0.4rem;"><strong>横幅卡片</strong></figcaption></figure>
  <figure style="width: 200px; margin: 0;"><img src="/setting-up-pushwoosh-inboxkit-ios-card-captioned.webp" alt="InboxKit 带标题卡片" style="width: 100%; border-radius: 12px;"/><figcaption style="text-align: center; font-size: 0.85rem; color: #94a3b8; margin-top: 0.4rem;"><strong>带标题卡片</strong></figcaption></figure>
  <figure style="width: 200px; margin: 0;"><img src="/setting-up-pushwoosh-inboxkit-ios-card-classic.webp" alt="InboxKit 经典卡片" style="width: 100%; border-radius: 12px;"/><figcaption style="text-align: center; font-size: 0.85rem; color: #94a3b8; margin-top: 0.4rem;"><strong>经典卡片</strong></figcaption></figure>
  <figure style="width: 200px; margin: 0;"><img src="/setting-up-pushwoosh-inboxkit-ios-card-carousel.webp" alt="InboxKit 轮播卡片" style="width: 100%; border-radius: 12px;"/><figcaption style="text-align: center; font-size: 0.85rem; color: #94a3b8; margin-top: 0.4rem;"><strong>轮播卡片</strong></figcaption></figure>
  <figure style="width: 200px; margin: 0;"><img src="/setting-up-pushwoosh-inboxkit-ios-card-video.webp" alt="InboxKit 视频卡片" style="width: 100%; border-radius: 12px;"/><figcaption style="text-align: center; font-size: 0.85rem; color: #94a3b8; margin-top: 0.4rem;"><strong>视频卡片</strong></figcaption></figure>
  <figure style="width: 200px; margin: 0;"><img src="/setting-up-pushwoosh-inboxkit-ios-card-wallet.webp" alt="InboxKit Apple Wallet 卡片" style="width: 100%; border-radius: 12px;"/><figcaption style="text-align: center; font-size: 0.85rem; color: #94a3b8; margin-top: 0.4rem;"><strong>Apple Wallet 卡片</strong></figcaption></figure>
</div>

横幅、带标题和经典卡片由标准消息字段（图片、标题、正文）以及可选的 `buttons` 数组驱动——请参见[添加内联 CTA 按钮](#add-inline-cta-buttons)。轮播、视频和 Apple Wallet 卡片在 `data` 内部携带额外的结构化数据，具体如下所述。

### 轮播卡片

轮播卡片可从单条消息中渲染多张图片——一个可滑动的图库，带有可选的每张幻灯片标题和点击目标。幻灯片位于 `data.carousel` 中。每张幻灯片都需要一个 `image`；`title`（标题覆盖层）和 `url`（点击时打开的深层链接）是可选的。没有图片的幻灯片将被丢弃；点击没有 `url` 的幻灯片会回退到消息的默认行操作。

```json title="POST https://api.pushwoosh.com/json/1.3/createMessage"
{
  "request": {
    "application": "XXXXX-XXXXX",
    "auth": "API_TOKEN",
    "notifications": [{
      "send_date": "now",
      "ios_title": "New arrivals",
      "content": "Swipe through this week's drops",
      "inbox_days": 7,
      "data": {
        "displayType": "carousel",
        "carousel": [
          { "image": "https://cdn.example.com/inbox/1.jpg", "title": "New in", "url": "myapp://product/1" },
          { "image": "https://cdn.example.com/inbox/2.jpg", "title": "On sale", "url": "myapp://product/2" },
          { "image": "https://cdn.example.com/inbox/3.jpg" }
        ]
      },
      "platforms": [1]
    }]
  }
}
```

### 视频卡片

视频卡片显示一张带有播放徽章的海报图片；点击它会打开一个全屏播放器（即使在静音开关打开的情况下也有声音）。描述符位于 `data.video` 中：`url` 是必需的，并且必须是 `http`/`https` 流或文件；`poster` 是一个可选的预览图片。

```json title="POST https://api.pushwoosh.com/json/1.3/createMessage"
{
  "request": {
    "application": "XXXXX-XXXXX",
    "auth": "API_TOKEN",
    "notifications": [{
      "send_date": "now",
      "ios_title": "Watch the reveal",
      "content": "Tap to play",
      "inbox_days": 7,
      "data": {
        "displayType": "video",
        "video": {
          "url": "https://cdn.example.com/inbox/clip.mp4",
          "poster": "https://cdn.example.com/inbox/poster.jpg"
        }
      },
      "platforms": [1]
    }]
  }
}
```

### Apple Wallet 卡片

Apple Wallet 卡片在官方的**添加到 Apple Wallet** 按钮上方显示可选的主图、标题和正文。点击按钮会下载 `.pkpass` 并呈现系统添加通行证的界面。用它来直接从收件箱发送优惠券、会员卡、门票或登机牌。该卡片仅适用于 iOS / Mac Catalyst——在其他平台上，消息会渲染为经典卡片。

通行证 URL 位于 `data.wallet` 中，可以是一个裸字符串，也可以是一个带有 `pass` 字段的对象。可选的 `data.image` 添加主图。当没有通行证 URL 或设备无法添加通行证时，按钮会自动隐藏。

```json title="POST https://api.pushwoosh.com/json/1.3/createMessage"
{
  "request": {
    "application": "XXXXX-XXXXX",
    "auth": "API_TOKEN",
    "notifications": [{
      "send_date": "now",
      "ios_title": "Your loyalty card is ready",
      "content": "Add it to Apple Wallet in one tap",
      "inbox_days": 7,
      "data": {
        "displayType": "wallet",
        "image": "https://cdn.example.com/inbox/loyalty.png",
        "wallet": "https://passes.example.com/v1/passes/pass.com.example.loyalty/abc123?token=…"
      },
      "platforms": [1]
    }]
  }
}
```

结果会报告给您的委托：

```swift
extension MyInboxHost: PushwooshInboxKitDelegate {

    func inboxKit(_ vc: PushwooshInboxKitViewController,
                  didAddWalletPassFor message: PWInboxMessageProtocol) {
        // 通行证现在在用户的 Wallet 中——如果需要，可以显示一个确认信息。
    }

    func inboxKit(_ vc: PushwooshInboxKitViewController,
                  didFailToAddWalletPassFor message: PWInboxMessageProtocol,
                  error: Error?) {
        // 下载失败——提供重试、记录日志等。
    }
}
```

两个回调都是可选的（它们带有默认的空实现）。用户取消系统界面既不是成功也不是失败，因此在这种情况下不会触发任何回调。

## 辅助功能

InboxKit 单元格开箱即支持 VoiceOver。横幅、带标题和经典卡片通过其底层的标签公开其标题、正文和日期，内联 CTA 按钮会读出自己的标题。富文本卡片添加了明确的语义：

- **视频** — 海报被公开为单个按钮元素，标记为“播放视频”（特征为 `.button` + `.startsMediaSession`），因此 VoiceOver 会将其作为媒体控件而不是普通图片来播报。
- **轮播** — 每个幻灯片都是一个按钮元素，其辅助功能标签是幻灯片的标题，如果没有标题则是“幻灯片”。页面指示器会播报当前位置为“第 *n* 页，共 *total* 页”。
- **Apple Wallet** — **添加到 Apple Wallet** 按钮是 Apple 的标准 `PKAddPassButton`，它带有自己的本地化 VoiceOver 标签。

对于 UI 测试和自动化，设置了两个稳定的 `accessibilityIdentifier`：视频海报上的 `inboxkit.video.play` 和 Wallet 按钮上的 `inboxkit.wallet.add`。

## 从消息中读取自定义数据

要使推送出现在收件箱中，[Messages API](/zh/developer/api-reference/messages-api/) 的 `createMessage` 请求必须包含 `inbox_image`、`inbox_date` 或 `inbox_days`——没有这些字段之一，推送将作为常规通知发送，永远不会到达收件箱信息流。自由格式的自定义数据放在 `data` 键下，SDK 会将其作为 `u` 参数传递给客户端：

```json title="POST https://api.pushwoosh.com/json/1.3/createMessage"
{
  "request": {
    "application": "XXXXX-XXXXX",
    "auth": "API_TOKEN",
    "notifications": [{
      "send_date": "now",
      "ios_title": "Summer sale",
      "content": "30% off everything — limited time only",
      "inbox_image": "https://cdn.example.com/inbox/summer.png",
      "inbox_days": 7,
      "data": {
        "displayType": "captioned",
        "promo_id": "SUMMER2026",
        "screen": "promo_details"
      },
      "platforms": [1]
    }]
  }
}
```

SDK 通过 `actionParams` 在收件箱消息上公开该对象。当用户点击行或内联 CTA 时，从委托中读取它：

```swift
extension MyInboxHost: PushwooshInboxKitDelegate {

    func inboxKit(_ vc: PushwooshInboxKitViewController,
                  didSelect message: PWInboxMessageProtocol) -> Bool {
        guard let params = message.actionParams as? [String: Any] else { return true }

        // 自定义 `data` 对象在 "u" 键下到达——
        // 可以是嵌套的字典，也可以是 JSON 编码的字符串，
        // 具体取决于 payload 在上游的构建方式。
        let custom: [String: Any]? = {
            if let dict = params["u"] as? [String: Any] { return dict }
            if let raw = params["u"] as? String,
               let bytes = raw.data(using: .utf8),
               let parsed = try? JSONSerialization.jsonObject(with: bytes) as? [String: Any] {
                return parsed
            }
            return nil
        }()

        if let promoId = custom?["promo_id"] as? String {
            navigateToPromo(promoId)
            return false   // 我们处理了点击；SDK 不应运行默认操作
        }
        return true
    }
}
```

对于内联 CTA 按钮，在 `inboxKit(_:didTapButton:onMessage:)` 内部使用相同的 `actionParams["u"]` 查找。对于类型化的 CTA 情况（`openURL`、`dismiss`、`markRead`），SDK 已经执行了默认操作——返回 `true` 以保留该行为，或返回 `false` 以禁止它并运行您自己的逻辑。

## 添加内联 CTA 按钮

一条消息最多可以携带三个内联的行动号召（call-to-action）按钮。按钮与其他自定义数据一起位于 `data` 内部，作为一个 `buttons` 数组。SDK 会在带标题和经典单元格内自动渲染它们：

```json title="POST https://api.pushwoosh.com/json/1.3/createMessage"
{
  "request": {
    "application": "XXXXX-XXXXX",
    "auth": "API_TOKEN",
    "notifications": [{
      "send_date": "now",
      "ios_title": "New promo card",
      "content": "Tap a button to claim or save",
      "inbox_image": "https://cdn.example.com/inbox/promo.png",
      "inbox_days": 7,
      "data": {
        "displayType": "captioned",
        "promo_id": "SUMMER2026",
        "buttons": [
          { "title": "Claim", "url": "https://example.com/promo/SUMMER2026" },
          { "title": "Read",  "action": "markRead" },
          { "title": "Save",  "action": "custom", "tag": "save_promo" }
        ]
      },
      "platforms": [1]
    }]
  }
}
```

每个按钮对象都有以下字段：

| 字段 | 类型 | 时间 |
|---|---|---|
| `title` | string | 必需。可见的按钮标签。 |
| `url` | string | 一个非空的可解析 URL 会产生一个 `openURL` 操作。除非您的委托禁止，否则 SDK 会通过 `UIApplication.shared.open` 打开它。 |
| `action` | string | 显式操作令牌：`dismiss`（从信息流中移除消息）、`markRead`（将消息标记为已读）或 `custom`（由宿主处理）。不区分大小写。 |
| 其他任何字段 | any | 当 `action` 为 `custom` 时，按钮对象上除 `title` 和 `action` 之外的每个键都会作为自定义 payload 转发给您的委托——与营销人员商定一个键（例如 `tag`）并据此进行分派。 |

解析优先级：首先是显式的 `action` 令牌，然后如果 `url` 非空则使用 `url`，否则按钮会回退到 `custom`，并携带完整的 payload（减去 `title` 和 `action`）。

从您的委托中拦截点击。`button.action` 属性是类型化的 `PushwooshInboxButtonAction` 枚举：

```swift
extension MyInboxHost: PushwooshInboxKitDelegate {

    func inboxKit(_ vc: PushwooshInboxKitViewController,
                  didTapButton button: PushwooshInboxButton,
                  onMessage message: PWInboxMessageProtocol) -> Bool {
        switch button.action {
        case .openURL(let url):
            // 默认行为没问题——让 SDK 打开 URL。
            return true

        case .dismiss, .markRead:
            // SDK 处理这两种情况。如果想覆盖，返回 false。
            return true

        case .custom(let payload):
            // 营销人员定义的自定义按钮。根据你们商定的键进行分派。
            if let tag = payload["tag"] as? String {
                switch tag {
                case "save_promo":
                    saveCurrentPromoLocally(message: message)
                default:
                    break
                }
            }
            return true   // 对于 custom 会被忽略——SDK 在这里从不运行默认操作
        }
    }
}
```