如何开发可供其他 iOS 开发者集成的支付 library?
结论
可以。iOS 可以把支付能力封装成可复用的 SDK。其他开发者只需完成集成并调用公开接口,不必修改 SDK 源码。
常见的交付形式有:
Swift Package.xcframework- CocoaPods 包
- 静态或动态 framework
页面跳转最好遵循一个明确的职责边界:SDK 负责支付页面和支付流程,宿主 App 负责支付完成后的业务导航。 SDK 不应查找或直接跳转到 Developer A 的 ConfirmViewController,而应通过 closure、delegate 或异步方法返回支付结果,再由 Developer A 决定后续展示哪个页面。
推荐的整体设计
一次典型的调用流程如下:
- Developer A 将支付 SDK 集成到 App。
- 宿主 App 创建 SDK 提供的支付页面。
- 宿主 App 使用
present或pushViewController展示该页面。 - SDK 收集支付信息,并向支付服务发起请求。
- SDK 通过回调返回成功、失败或取消结果。
- 宿主 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 工程内部验证。
可参考的官方资料
- Creating a standalone Swift package with Xcode
- Distributing binary frameworks as Swift packages
- Creating a multiplatform binary framework bundle
- StoreKit
- App Review Guidelines
- Universal Links
这种 library 完全可行,但不应由它“跳转到 Developer A 的确认页面”。合理的职责划分是:SDK 提供支付界面、执行支付流程并返回明确结果,Developer A 的 App 收到结果后自行关闭支付页面,再进入自己的确认页面。
备注:内容仅供参考。
