关于 financekit
面向 Apple 平台应用开发者的 FinanceKit 指南,介绍如何在用户授权后访问 Apple Wallet 中符合条件的账户、余额和交易数据。还涵盖 Wallet 订单、交易选择器与后台数据更新,并说明相关资格和授权限制。
主要功能
- 01请求金融数据授权
- 02查询账户、余额和交易
- 03使用历史记录进行增量同步
- 04通过 TransactionPicker 选择交易
- 05保存和查询 Wallet 订单
- 06注册金融数据后台更新
适用场景
- 01在预算应用中汇总账户余额
- 02同步交易以更新支出记录
- 03让用户临时选择要导入的交易
- 04将订单保存到 Apple Wallet
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill financekit安装后在 Claude Code 中输入/financekit确认已加载。
附带文件evals/evals.jsonreferences/financekit-patterns.md
详细介绍
FinanceKit
访问 Apple Wallet 中符合条件的金融数据,包括美国的 Apple Card、Apple Cash、Savings,以及英国的已连接账户数据。FinanceKit 可在设备上访问账户、余额和交易记录,并由用户控制授权。支持 Swift 6.3 / 当前 Apple 平台;查询 API 从 iOS/iPadOS 17.4 起提供,TransactionPicker 从 iOS/iPadOS 18 起提供,后台投递从 iOS/iPadOS 26 起提供。
FinanceKit 指南应聚焦于金融数据访问、Wallet 订单存储与查询、TransactionPicker 和后台投递。Apple Pay 结账应使用 PassKit;Widget 界面与时间线工作应使用 WidgetKit;Wallet 订单追踪邮件或 Apple Business Connect 优化不属于此技能的范围。
目录
设置与权利
要求
- 受管理的权利 -- 通过 FinanceKit 权利申请表向 Apple 申请
com.apple.developer.financekit。这是一项受管理的功能;Apple 会审核每个应用。 - 组织级 Apple Developer 账户(个人账户不符合条件)。
- 申请此权利需要账户持有人角色。
- 符合条件的 App Store 应用 -- 应用必须属于 Finance 类别,通过 App Store 在美国或英国面向 iPhone 分发,并提供净资产、支出或预算等财务管理工具。
- 按 Bundle ID 审批 -- Apple 会将此权利分配给获批的 Bundle ID;不要假定它会自动适用于同一系列的其他应用或扩展。
- 如果应用直接或通过受监管的机构提供金融产品,则必须允许客户将这些账户连接到 Apple Wallet,并通过 FinanceKit 共享数据。
项目配置
- Apple 批准申请后,通过 Xcode 受管理的功能添加 FinanceKit 权利。
- 将
NSFinancialDataUsageDescription添加到 Info.plist -- 此字符串会在授权提示中向用户显示。 - 对于 iOS 26 后台投递,将 FinanceKit 权利添加到应用和扩展目标,然后使用 App Groups 进行共享存储。
<key>NSFinancialDataUsageDescription</key>
<string>This app uses your financial data to track spending and provide budgeting insights.</string>
数据可用性
美国的 FinanceKit 金融数据要求 iOS/iPadOS 17.4+,目前涵盖符合条件的 Apple Card、Apple Cash 和 Savings 数据;Apple Card Family 参与者和 Apple Cash Family 子女不包含在内。英国支持要求 iOS/iPadOS 18.4+,并使用开放银行服务访问受支持机构的数据。订单 API 与金融数据查询 API 分开提供。
在进行任何 API 调用之前,请检查设备是否支持 FinanceKit。此值在不同启动和 iOS 版本之间保持不变。
import FinanceKit
guard FinanceStore.isDataAvailable(.financialData) else {
// FinanceKit not available -- do not call any other financial data APIs.
// The framework terminates the app if called when unavailable.
return
}
对于 Wallet 订单:
guard FinanceStore.isDataAvailable(.orders) else { return }
数据可用性返回 true 并不保证设备上存在数据。数据访问也可能暂时受限(例如,Wallet 不可用、MDM 限制)。访问受限时会抛出 FinanceError.dataRestricted,而不会导致程序终止。
授权
请求授权以访问用户选择的金融账户。系统会显示账户选择器,用户可在其中选择要共享的账户以及可公开的最早交易日期。
let store = FinanceStore.shared
let status = try await store.requestAuthorization()
switch status {
case .authorized: break // Proceed with queries
case .denied: break // User declined
case .notDetermined: break // No meaningful choice made
@unknown default: break
}
检查当前状态
查询当前授权状态,不显示提示:
let currentStatus = try await store.authorizationStatus()
用户授予或拒绝访问权限后,requestAuthorization() 会返回缓存的决定,不会再次显示提示。用户可以在“设置”>“隐私与安全性”>“财务数据”中更改访问权限。
查询账户
账户建模为一个包含两个 case 的枚举:.asset(例如 Apple Cash、储蓄账户)和 .liability(例如 Apple Card 信用账户)。两者共享通用属性(id、displayName、institutionName、currencyCode),而负债账户还包含信用相关字段。
func fetchAccounts() async throws -> [Account] {
let query = AccountQuery(
sortDescriptors: [SortDescriptor(\Account.displayName)],
predicate: nil,
limit: nil,
offset: nil
)
return try await store.accounts(query: query)
}
使用账户类型
switch account {
case .asset(let asset):
print("Asset account, currency: \(asset.currencyCode)")
case .liability(let liability):
if let limit = liability.creditInformation.creditLimit {
print("Credit limit: \(limit.amount) \(limit.currencyCode)")
}
}
账户余额
余额表示账户在某个时间点的金额。CurrentBalance 有三个 case:.available(包含待处理金额)、.booked(仅包含已入账金额)或 .availableAndBooked。
func fetchBalances(for accountID: UUID) async throws -> [AccountBalance] {
let predicate = #Predicate<AccountBalance> { balance in
balance.accountID == accountID
}
let query = AccountBalanceQuery(
sortDescriptors: [SortDescriptor(\AccountBalance.id)],
predicate: predicate,
limit: nil,
offset: nil
)
return try await store.accountBalances(query: query)
}
读取余额金额
金额始终为正数。使用 creditDebitIndicator 确定正负:
func formatBalance(_ balance: Balance) -> String {
let sign = balance.creditDebitIndicator == .debit ? "-" : ""
return "\(sign)\(balance.amount.amount) \(balance.amount.currencyCode)"
}
// Extract from CurrentBalance enum:
switch balance.currentBalance {
case .available(let bal): formatBalance(bal)
case .booked(let bal): formatBalance(bal)
case .availableAndBooked(let available, _): formatBalance(available)
@unknown default: "Unknown"
}
查询交易
使用 TransactionQuery 配合 Swift 谓词、排序描述符、限制和偏移量。
let predicate = #Predicate<Transaction> { $0.accountID == accountID }
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate,
limit: 50,
offset: nil
)
let transactions = try await store.transactions(query: query)
读取交易数据
let amount = transaction.transactionAmount
let direction = transaction.creditDebitIndicator == .debit ? "spent" : "received"
print("\(transaction.transactionDescription): \(direction) \(amount.amount) \(amount.currencyCode)")
// merchantName, merchantCategoryCode, foreignCurrencyAmount are optional
内置谓词辅助方法
FinanceKit 提供了用于常见筛选条件的工厂方法:
// Filter by transaction status
let bookedOnly = TransactionQuery.predicate(forStatuses: [.booked])
// Filter by transaction type
let purchases = TransactionQuery.predicate(forTransactionTypes: [.pointOfSale, .directDebit])
// Filter by merchant category
let groceries = TransactionQuery.predicate(forMerchantCategoryCodes: [
MerchantCategoryCode(rawValue: 5411) // Grocery stores
])
如需查看交易字段表和更多查询模式,请阅读 references/financekit-patterns.md。
长时间运行的查询和历史记录
使用基于 AsyncSequence 的历史记录 API 进行追赶式同步、实时更新或可恢复同步。这些 API 会返回已插入、已更新和已删除条目的 ID,以及一个 HistoryToken。
func catchUpTransactions(for accountID: UUID) async throws {
let history = store.transactionHistory(
forAccountID: accountID,
since: loadSavedToken(),
isMonitoring: false // finish after saved-token catch-up
)
for try await changes in history {
removeLocalRecords(withIDs: changes.deleted)
upsert(changes.inserted + changes.updated)
saveToken(changes.newToken)
}
}
持久化历史记录令牌
HistoryToken 遵循 Codable。将其持久化,以便恢复查询而无需重新处理数据:
func saveToken(_ token: FinanceStore.HistoryToken) {
if let data = try? JSONEncoder().encode(token) {
UserDefaults.standard.set(data, forKey: "financeHistoryToken")
}
}
func loadSavedToken() -> FinanceStore.HistoryToken? {
guard let data = UserDefaults.standard.data(forKey: "financeHistoryToken") else { return nil }
return try? JSONDecoder().decode(FinanceStore.HistoryToken.self, from: data)
}
如果保存的令牌指向已压缩的历史记录,框架会抛出 FinanceError.historyTokenInvalid。丢弃该令牌,然后立即对受影响的账户或余额数据流运行一次全新的追赶式查询,以重建本地状态和替换令牌。仅对单独的实时监视器使用 isMonitoring: true。
账户和余额历史记录
let accountChanges = store.accountHistory(since: nil, isMonitoring: true)
let balanceChanges = store.accountBalanceHistory(forAccountID: accountID, since: nil, isMonitoring: true)
持续进行的预算同步应涵盖用户授权的数据模型:用于账户新增和移除的账户对象、用于趋势和小组件状态的账户余额,以及用于支出明细的交易。为每个数据流或账户使用单独的历史记录令牌,这样令牌压缩只会要求重新同步受影响的数据流。
交易选择器
对于需要选择性、临时访问而无需完整授权的应用,请使用 FinanceKitUI 中的 TransactionPicker。访问权限不会持久化——交易会直接传递,以供立即使用。
import FinanceKitUI
struct ExpenseImportView: View {
@State private var selectedTransactions: [Transaction] = []
var body: some View {
if FinanceStore.isDataAvailable(.financialData) {
TransactionPicker(selection: $selectedTransactions) {
Label("Import Transactions", systemImage: "creditcard")
}
}
}
}
Wallet 订单
FinanceKit 支持保存和查询 Wallet 订单(例如购买收据、配送跟踪)。
保存订单
let result = try await store.saveOrder(signedArchive: archiveData)
switch result {
case .added: break // Saved
case .cancelled: break // User cancelled
case .newerExisting: break // Newer version already in Wallet
@unknown default: break
}
检查是否已有订单
let orderID = FullyQualifiedOrderIdentifier(
orderTypeIdentifier: "com.merchant.order",
orderIdentifier: "ORDER-123"
)
let result = try await store.containsOrder(matching: orderID, updatedDate: lastKnownDate)
// result: .exists, .newerExists, .olderExists, or .notFound
“添加到 Wallet”按钮(FinanceKitUI)
import FinanceKitUI
AddOrderToWalletButton(signedArchive: orderData) { result in
// result: .success(SaveOrderResult) or .failure(Error)
}
后台传送
iOS 26+ 支持后台传送扩展,可在应用生命周期之外通知应用财务数据变更。在主应用中获得的用户授权会继承到扩展。两个目标都需要 FinanceKit 权限;使用 App Groups 在应用、扩展和相关小组件之间共享数据。
启用后台传送
这些注册方法是同步且不会抛出错误的;不要使用 try 或 await。
store.enableBackgroundDelivery(
for: [.accounts, .accountBalances, .transactions],
frequency: .daily
)
可用频率:.hourly、.daily、.weekly。这些频率表示数据变更时扩展启动之间预期的最短间隔;频率越低,扩展获得的处理窗口越大。
选择性或完全禁用:
store.disableBackgroundDelivery(for: [.transactions])
store.disableAllBackgroundDelivery()
后台传送扩展
在 Xcode 中创建后台传送扩展目标(Background Delivery Extension 模板)。直接在扩展类型上实现两个异步入口点,并且仅在保存好必要工作后,才从 didReceiveData(for:) 返回。
import FinanceKit
@main
struct MyFinanceExtension: BackgroundDeliveryExtension {
func didReceiveData(for types: [FinanceStore.BackgroundDataType]) async {
if types.contains(.transactions) {
await processNewTransactions()
}
if types.contains(.accountBalances) {
await updateBalanceCache()
}
if types.contains(.accounts) {
await refreshAccountList()
}
}
func willTerminate() async { await savePartialWork() }
}
常见错误
1. 数据不可用时调用 API
不要这样做——跳过可用性检查:
let store = FinanceStore.shared
let status = try await store.requestAuthorization() // Terminates if unavailable
应该这样做——先检查可用性:
guard FinanceStore.isDataAvailable(.financialData) else {
showUnavailableMessage()
return
}
let status = try await FinanceStore.shared.requestAuthorization()
2. 忽略借记/贷记指示符
不要这样做——将金额视为带符号的值:
let spent = transaction.transactionAmount.amount // Always positive
应该这样做——应用指示符:
let amount = transaction.transactionAmount.amount
let signed = transaction.creditDebitIndicator == .debit ? -amount : amount
3. 未处理数据限制错误
不要这样做——假定授权会持续有效:
let transactions = try await store.transactions(query: query) // Fails if Wallet restricted
应该这样做——捕获 FinanceError:
do {
let transactions = try await store.transactions(query: query)
} catch let error as FinanceError {
if case .dataRestricted = error { showDataRestrictedMessage() }
}
4. 用快照替代可恢复的历史记录
使用上文规范的交易历史循环,并仅在本地删除和更新操作提交后,才持久化每个 newToken。为每个数据流/账户分别保存令牌;遇到无效令牌错误时,仅重新同步受影响的范围。
5. 误解负债账户上的借记/贷记
资产账户和负债账户都使用 .debit 表示资金流出。但 .credit 的含义不同:在资产账户上表示收到资金;在负债账户上表示还款或退款,使可用信用额度增加。完整的解读表请参阅 references/financekit-patterns.md。
检查清单
- 在任何 API 调用前检查
FinanceStore.isDataAvailable(.financialData) - 检查应用资格:Finance 类别、面向美国或英国用户通过 App Store 分发的 iPhone 应用、财务管理功能集、组织账户、Account Holder 申请
- 为应用的 bundle ID 申请并获批
com.apple.developer.financekit权限 - 在 Info.plist 中设置
NSFinancialDataUsageDescription,并填写清晰、具体的说明 - 处理所有授权状态(
.authorized、.denied、.notDetermined) - 捕获并妥善处理
FinanceError.dataRestricted - 正确应用
CreditDebitIndicator处理金额(而不是将其视为带符号的值) - 持久化历史记录令牌,以支持可恢复查询
- 遇到
FinanceError.historyTokenInvalid时,丢弃令牌并立即重新同步受影响的数据流 - 持续同步计划涵盖已授权的账户、余额和交易,而不只是交易
- 不需要实时更新时,长时间运行的查询使用
isMonitoring: false - 不需要完整授权时,使用交易选择器
- 只查询应用确实需要的数据
- 明确从本地账户、余额或交易存储中删除历史记录变更返回的 ID
- 后台传输调用使用同步的 iOS 26 API,并确保扩展与主应用位于同一 App Group 中
- 后台传输注册所有需要的数据类型:
.accounts、.accountBalances和/或.transactions - 同时为应用和后台传输扩展目标添加 FinanceKit 权限
- 用户撤销访问权限时删除财务数据