PAYATHON 2026

如何开发可供其他 iOS 开发者集成的支付 library?

支付小周

结论

可以。iOS 可以把支付能力封装成可复用的 SDK。其他开发者只需完成集成并调用公开接口,不必修改 SDK 源码。

常见的交付形式有:

  • Swift Package
  • .xcframework
  • CocoaPods 包
  • 静态或动态 framework

页面跳转最好遵循一个明确的职责边界:SDK 负责支付页面和支付流程,宿主 App 负责支付完成后的业务导航。 SDK 不应查找或直接跳转到 Developer A 的 ConfirmViewController,而应通过 closure、delegate 或异步方法返回支付结果,再由 Developer A 决定后续展示哪个页面。

推荐的整体设计

一次典型的调用流程如下:

  1. Developer A 将支付 SDK 集成到 App。
  2. 宿主 App 创建 SDK 提供的支付页面。
  3. 宿主 App 使用 present 或 pushViewController 展示该页面。
  4. SDK 收集支付信息,并向支付服务发起请求。
  5. SDK 通过回调返回成功、失败或取消结果。
  6. 宿主 App 关闭支付页面,再跳转到自己的确认页面。

这样一来,SDK 不需要依赖宿主 App 的页面名称、导航结构或具体业务实现,更便于复用和维护。

对外公开的 API

可以先定义支付请求、支付结果和错误类型:

import UIKit

public struct PaymentRequest {
    public let amount: Decimal
    public let currency: String
    public let orderID: String

    public init(
        amount: Decimal,
        currency: String,
        orderID: String
    ) {
        self.amount = amount
        self.currency = currency
        self.orderID = orderID
    }
}

public struct PaymentReceipt {
    public let transactionID: String
    public let orderID: String

    public init(transactionID: String, orderID: String) {
        self.transactionID = transactionID
        self.orderID = orderID
    }
}

public enum PaymentError: Error {
    case invalidConfiguration
    case invalidPaymentDetails
    case networkFailure
    case declined(message: String)
    case unknown
}

public enum PaymentResult {
    case success(PaymentReceipt)
    case failure(PaymentError)
    case cancelled
}

然后提供创建支付页面的入口:

public final class PaymentSDK {
    public static let shared = PaymentSDK()

    private init() {}

    public func configure(apiKey: String) {
        // 保存 SDK 配置。
        // 不要把支付服务的 secret key 放在客户端。
    }

    public func makePaymentViewController(
        request: PaymentRequest,
        completion: @escaping (PaymentResult) -> Void
    ) -> UIViewController {
        PaymentViewController(
            request: request,
            completion: completion
        )
    }
}

支付页面可以保留为 SDK 的内部实现,以免宿主 App 依赖尚未稳定的内部类型:

final class PaymentViewController: UIViewController {
    private let request: PaymentRequest
    private let completion: (PaymentResult) -> Void
    private var hasCompleted = false

    init(
        request: PaymentRequest,
        completion: @escaping (PaymentResult) -> Void
    ) {
        self.request = request
        self.completion = completion
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

        // 创建信用卡输入控件、支付按钮和取消按钮。
    }

    private func finish(with result: PaymentResult) {
        guard !hasCompleted else {
            return
        }

        hasCompleted = true
        completion(result)
    }

    private func paymentSucceeded(transactionID: String) {
        let receipt = PaymentReceipt(
            transactionID: transactionID,
            orderID: request.orderID
        )

        finish(with: .success(receipt))
    }

    private func paymentFailed(_ error: PaymentError) {
        finish(with: .failure(error))
    }

    @objc private func cancelPayment() {
        finish(with: .cancelled)
    }
}

hasCompleted 用于防止网络回调、取消操作或页面事件多次触发 completion。正式 SDK 还应明确保证回调在主线程执行。

宿主 App 如何集成

Developer A 可以在自己的页面中展示 SDK 的支付界面:

let request = PaymentRequest(
    amount: Decimal(string: "99.00")!,
    currency: "CNY",
    orderID: "ORDER-20260901-001"
)

let paymentViewController =
    PaymentSDK.shared.makePaymentViewController(request: request) {
        [weak self] result in

        DispatchQueue.main.async {
            guard let self else {
                return
            }

            self.dismiss(animated: true) {
                switch result {
                case .success(let receipt):
                    let confirmViewController = ConfirmViewController(
                        transactionID: receipt.transactionID
                    )
                    self.navigationController?.pushViewController(
                        confirmViewController,
                        animated: true
                    )

                case .failure(let error):
                    self.showPaymentError(error)

                case .cancelled:
                    break
                }
            }
        }
    }

let navigationController = UINavigationController(
    rootViewController: paymentViewController
)

present(navigationController, animated: true)

这里最重要的是,ConfirmViewController 完全属于 Developer A。SDK 不需要知道这个页面是否存在,也不用关心宿主 App 采用 UIKit、SwiftUI、导航控制器还是其他路由方案。

如果宿主 App 已经使用 UINavigationController,也可以直接执行:

navigationController?.pushViewController(
    paymentViewController,
    animated: true
)

不过,这种方式需要提前约定由谁负责返回和关闭页面,以及如何处理交互式侧滑返回。对第三方 SDK 而言,模态展示通常更容易划清边界。

也可以使用 delegate

如果支付过程会产生多个事件,例如开始验证、支付成功、支付失败和用户取消,delegate 比单个 closure 更合适:

public protocol PaymentViewControllerDelegate: AnyObject {
    func paymentViewController(
        _ viewController: UIViewController,
        didComplete receipt: PaymentReceipt
    )

    func paymentViewController(
        _ viewController: UIViewController,
        didFail error: PaymentError
    )

    func paymentViewControllerDidCancel(
        _ viewController: UIViewController
    )
}

closure 适合一次性返回最终结果,delegate 则适合连续通知多个事件。选择其中一种作为主要接口即可,不要同时提供多套行为不一致的回调方式。

使用 Swift Concurrency 的接口

如果 SDK 的最低系统版本支持 Swift Concurrency,也可以把支付请求封装成 async 方法:

public protocol PaymentProcessing {
    func process(_ request: PaymentRequest) async throws -> PaymentReceipt
}

即便如此,支付页面的取消、关闭和导航仍应由宿主 App 控制。如果 SDK 需要适配较多项目,可以先提供基于 closure 的基础接口,再按需增加 async 封装。

SDK 应采用哪种发布格式

Swift Package

如果 SDK 主要以源码形式交付,Swift Package Manager 通常是最简单的选择。开发者可以直接在 Xcode 中通过包地址添加依赖。

源码包意味着使用者通常能够看到实现。如果需求只是“不要求修改源码”,源码包完全可以满足。如果还要求“不公开源码”,则需要发布预编译的二进制包。

XCFramework

.xcframework 适合分发预编译 SDK,也可以同时包含真机和模拟器需要的二进制切片。不要把设备和模拟器生成的 framework 手工拼成一个普通 framework。

XCFramework 还可以作为 Swift Package 的二进制依赖发布:

.binaryTarget(
    name: "PaymentSDK",
    url: "https://example.com/PaymentSDK-1.0.0.xcframework.zip",
    checksum: "由 Swift Package Manager 计算得到的校验值"
)

如果 SDK 使用图片、字体、本地化文件或 storyboard,还需要正确打包这些资源,不能假定它们位于宿主 App 的 main bundle 中。源码 Swift Package 可以使用 Bundle.module,二进制 SDK 通常需要附带独立的 resource bundle,并由 SDK 自行解析。

支付功能必须注意的边界

数字内容不能任意使用信用卡支付

如果交易对象是在 App 内消费的数字内容或数字服务,例如会员权限、虚拟货币、游戏道具或内容解锁,通常需要使用 Apple 的 In-App Purchase,并通过 StoreKit 实现。

如果交易对象是实体商品或现实世界中的服务,例如电商商品、外卖、交通或线下预约,通常会使用第三方支付服务。具体业务是否符合要求,仍要以提交 App 时有效的 App Review Guidelines 和业务所在地区的规则为准。

不要自行保存完整信用卡信息

信用卡号、安全码等数据高度敏感。支付 SDK 不应把原始卡信息写入:

  • UserDefaults
  • 日志
  • 分析事件
  • 普通数据库
  • 未加密文件
  • 崩溃报告字段

更稳妥的做法是使用支付服务商提供的官方 iOS SDK 或安全输入组件,让卡信息直接完成 token 化。服务器和 SDK 应尽量只处理 payment token、payment method ID 或交易 ID。

自行收集、传输或存储卡数据可能扩大 PCI DSS 合规范围。仅在客户端加密,并不能自动解决合规问题。

Secret key 必须放在服务端

客户端应用可以被分析,因此不能在 SDK 中保存支付服务商的 secret key。常见架构如下:

宿主 App
   ↓
支付 SDK
   ↓
你的后端创建支付订单或支付意图
   ↓
支付服务商
   ↓
你的后端通过 webhook 确认最终状态

SDK 中只能保存允许公开的客户端配置,例如 publishable key。金额、币种、订单归属和最终支付状态都应由服务端校验,不能完全信任客户端提交的数据。

客户端成功回调不是最终结算凭证

SDK 收到成功响应后可以更新界面,但如果后续涉及发货、充值或开通权益,宿主业务系统仍应通过服务端查询或 webhook 验证交易状态。客户端回调可能受到网络中断、重复请求或篡改的影响,因而与实际结算结果不一致。

每次支付还应使用唯一订单号或幂等键,避免用户重复点击造成重复扣款。

页面和生命周期设计

第三方 SDK 不应假定:

  • 宿主 App 一定有某个类名的确认页面。
  • 当前页面一定包含 UINavigationController。
  • App 一定使用 UIKit。
  • SDK 可以直接访问 UIApplication.shared.keyWindow。
  • 支付完成后一定要自动关闭页面。

比较可靠的方式是让宿主 App 显式传入展示上下文,或者让 SDK 返回一个 UIViewController。支付结束后,SDK 只返回结构化结果,不直接操作宿主 App 的导航栈。

还需要处理以下情况:

  • 用户主动取消支付。
  • 网络请求超时。
  • App 进入后台后再次恢复。
  • 3D Secure 等验证流程跳转到浏览器或其他 App。
  • Universal Link 或自定义 URL Scheme 回跳。
  • 页面被交互式关闭。
  • 支付请求重复提交。
  • completion 被错误触发多次。

如果支付服务需要处理外部认证回跳,可以向宿主 App 提供明确的 URL 转发入口,例如:

public func handleCallbackURL(_ url: URL) -> Bool {
    // 检查 URL 是否属于当前支付流程,并继续认证。
    return false
}

宿主 App 再从自己的 URL 生命周期方法调用该入口。具体使用哪个方法,取决于宿主采用 AppDelegate、SceneDelegate 还是 SwiftUI App 生命周期。

API 稳定性和兼容性

对外只暴露必要的类型,支付页面的内部控件、网络模型和服务实现应保留为 internal 或 private。公开 API 发布后很难随意修改,因此需要提前约定:

  • SDK 初始化方式
  • 支付请求字段
  • 成功、失败和取消的含义
  • completion 所在线程
  • 页面由谁展示和关闭
  • 错误码是否稳定
  • 是否允许重试
  • 最低 iOS 版本
  • Swift、Objective-C 和 SwiftUI 的兼容范围

如果需要支持 Objective-C,公开模型不能只使用 Objective-C 无法表示的 Swift 类型。可以另外提供 NSObject、@objc protocol、错误码和适合 Objective-C 调用的入口。

二进制 Swift SDK 还要考虑模块稳定性,以及不同 Xcode 和 Swift 编译器版本之间的兼容问题。发布前应使用独立的示例 App 测试安装和调用流程,不能只在 SDK 工程内部验证。

可参考的官方资料

这种 library 完全可行,但不应由它“跳转到 Developer A 的确认页面”。合理的职责划分是:SDK 提供支付界面、执行支付流程并返回明确结果,Developer A 的 App 收到结果后自行关闭支付页面,再进入自己的确认页面。

备注:内容仅供参考。