financekit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

FinanceKit

FinanceKit

Access financial data from Apple Wallet including Apple Card, Apple Cash, and Apple Card Savings. FinanceKit provides on-device, offline access to accounts, balances, and transactions with user-controlled authorization. Targets Swift 6.3 / iOS 26+. Query APIs are available from iOS 17.4; background delivery requires iOS 26.
访问来自Apple Wallet的财务数据,包括Apple Card、Apple Cash和Apple Card Savings。FinanceKit提供设备端离线访问账户、余额和交易的能力,权限由用户控制。适配Swift 6.3 / iOS 26及以上版本。查询API从iOS 17.4开始可用;后台推送功能需要iOS 26及以上版本。

Contents

目录

Setup and Entitlements

设置与权限

Requirements

要求

  1. Managed entitlement -- request
    com.apple.developer.financekit
    from Apple via the FinanceKit entitlement request form. This is a managed capability; Apple reviews each application.
  2. Organization-level Apple Developer account (individual accounts are not eligible).
  3. Account Holder role required to request the entitlement.
  1. 托管权限——通过FinanceKit权限申请表向苹果申请
    com.apple.developer.financekit
    权限。这是一项托管能力,苹果会对每个申请进行审核。
  2. 组织级Apple Developer账户(个人账户不符合申请条件)。
  3. 需要具备账户持有人角色才能申请该权限。

Project Configuration

项目配置

  1. Add the FinanceKit entitlement through Xcode managed capabilities after Apple approves the request.
  2. Add
    NSFinancialDataUsageDescription
    to Info.plist -- this string is shown to the user during the authorization prompt.
xml
<key>NSFinancialDataUsageDescription</key>
<string>This app uses your financial data to track spending and provide budgeting insights.</string>
  1. 苹果批准申请后,通过Xcode托管能力添加FinanceKit权限。
  2. 在Info.plist中添加
    NSFinancialDataUsageDescription
    ——该字符串会在授权弹窗中展示给用户。
xml
<key>NSFinancialDataUsageDescription</key>
<string>This app uses your financial data to track spending and provide budgeting insights.</string>

Data Availability

数据可用性

Check whether the device supports FinanceKit before making any API calls. This value is constant across launches and iOS versions.
swift
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
}
For Wallet orders:
swift
guard FinanceStore.isDataAvailable(.orders) else { return }
Data availability returning
true
does not guarantee data exists on the device. Data access can also become temporarily restricted (e.g., Wallet unavailable, MDM restrictions). Restricted access throws
FinanceError.dataRestricted
rather than terminating.
调用任何API之前先检查设备是否支持FinanceKit。该值在应用启动和iOS版本间保持恒定。
swift
import FinanceKit

guard FinanceStore.isDataAvailable(.financialData) else {
    // FinanceKit不可用——请勿调用任何其他财务数据API。
    // 若不可用时调用框架,应用会被终止。
    return
}
针对Wallet订单:
swift
guard FinanceStore.isDataAvailable(.orders) else { return }
数据可用性返回
true
不代表设备上一定存在数据。数据访问也可能临时受限(例如Wallet不可用、MDM限制)。访问受限会抛出
FinanceError.dataRestricted
错误,不会终止应用。

Authorization

授权

Request authorization to access user-selected financial accounts. The system presents an account picker where the user chooses which accounts to share and the earliest transaction date to expose.
swift
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
}
请求访问用户选定的财务账户的权限。系统会展示账户选择器,用户可以选择要共享的账户以及可公开的最早交易日期。
swift
let store = FinanceStore.shared

let status = try await store.requestAuthorization()
switch status {
case .authorized:    break  // 可继续执行查询
case .denied:        break  // 用户拒绝了授权
case .notDetermined: break  // 用户未做出有效选择
@unknown default:    break
}

Checking Current Status

检查当前授权状态

Query current authorization without prompting:
swift
let currentStatus = try await store.authorizationStatus()
Once the user grants or denies access,
requestAuthorization()
returns the cached decision without showing the prompt again. Users can change access in Settings > Privacy & Security > Financial Data.
无需弹窗即可查询当前授权状态:
swift
let currentStatus = try await store.authorizationStatus()
用户授予或拒绝访问后,
requestAuthorization()
会返回缓存的决策,不会再次展示弹窗。用户可以在设置 > 隐私与安全性 > 财务数据中修改访问权限。

Querying Accounts

查询账户

Accounts are modeled as an enum with two cases:
.asset
(e.g., Apple Cash, Savings) and
.liability
(e.g., Apple Card credit). Both share common properties (
id
,
displayName
,
institutionName
,
currencyCode
) while liability accounts add credit-specific fields.
swift
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)
}
账户被建模为枚举,包含两个case:
.asset
(例如Apple Cash、储蓄账户)和
.liability
(例如Apple Card信用卡)。两者都共享通用属性(
id
displayName
institutionName
currencyCode
),而负债账户额外新增了信贷相关字段。
swift
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)
}

Working with Account Types

处理账户类型

swift
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)")
    }
}
swift
switch account {
case .asset(let asset):
    print("资产账户,货币:\(asset.currencyCode)")
case .liability(let liability):
    if let limit = liability.creditInformation.creditLimit {
        print("信用额度:\(limit.amount) \(limit.currencyCode)")
    }
}

Account Balances

账户余额

Balances represent the amount in an account at a point in time. A
CurrentBalance
is one of three cases:
.available
(includes pending),
.booked
(posted only), or
.availableAndBooked
.
swift
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)
}
余额代表某一时间点账户内的金额。
CurrentBalance
是三个case之一:
.available
(包含待处理交易)、
.booked
(仅已入账交易)或
.availableAndBooked
swift
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)
}

Reading Balance Amounts

读取余额数值

Amounts are always positive decimals. Use
creditDebitIndicator
to determine the sign:
swift
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"
}
金额始终为正十进制数。使用
creditDebitIndicator
确定符号:
swift
func formatBalance(_ balance: Balance) -> String {
    let sign = balance.creditDebitIndicator == .debit ? "-" : ""
    return "\(sign)\(balance.amount.amount) \(balance.amount.currencyCode)"
}

// 从CurrentBalance枚举中提取:
switch balance.currentBalance {
case .available(let bal):       formatBalance(bal)
case .booked(let bal):          formatBalance(bal)
case .availableAndBooked(let available, _): formatBalance(available)
@unknown default: "Unknown"
}

Querying Transactions

查询交易记录

Use
TransactionQuery
with Swift predicates, sort descriptors, limit, and offset.
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)
使用带Swift谓词、排序描述符、limit和offset的
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)

Reading Transaction Data

读取交易数据

swift
let amount = transaction.transactionAmount
let direction = transaction.creditDebitIndicator == .debit ? "spent" : "received"
print("\(transaction.transactionDescription): \(direction) \(amount.amount) \(amount.currencyCode)")
// merchantName, merchantCategoryCode, foreignCurrencyAmount are optional
swift
let amount = transaction.transactionAmount
let direction = transaction.creditDebitIndicator == .debit ? "支出" : "收入"
print("\(transaction.transactionDescription): \(direction) \(amount.amount) \(amount.currencyCode)")
// merchantName、merchantCategoryCode、foreignCurrencyAmount为可选值

Built-In Predicate Helpers

内置谓词助手

FinanceKit provides factory methods for common filters:
swift
// 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
])
FinanceKit为常见筛选场景提供了工厂方法:
swift
// 按交易状态筛选
let bookedOnly = TransactionQuery.predicate(forStatuses: [.booked])

// 按交易类型筛选
let purchases = TransactionQuery.predicate(forTransactionTypes: [.pointOfSale, .directDebit])

// 按商户类别筛选
let groceries = TransactionQuery.predicate(forMerchantCategoryCodes: [
    MerchantCategoryCode(rawValue: 5411)  // 杂货店
])

Transaction Properties Reference

交易属性参考

PropertyTypeNotes
id
UUID
Unique per device
accountID
UUID
Links to parent account
transactionDate
Date
When the transaction occurred
postedDate
Date?
When booked; nil if pending
transactionAmount
CurrencyAmount
Always positive
creditDebitIndicator
CreditDebitIndicator
.debit
or
.credit
transactionDescription
String
Display-friendly description
originalTransactionDescription
String
Raw institution description
merchantName
String?
Merchant name if available
merchantCategoryCode
MerchantCategoryCode?
ISO 18245 code
transactionType
TransactionType
.pointOfSale
,
.transfer
, etc.
status
TransactionStatus
.authorized
,
.pending
,
.booked
,
.memo
,
.rejected
foreignCurrencyAmount
CurrencyAmount?
Foreign currency if applicable
foreignCurrencyExchangeRate
Decimal?
Exchange rate if applicable
属性类型说明
id
UUID
设备内唯一
accountID
UUID
关联所属账户
transactionDate
Date
交易发生时间
postedDate
Date?
入账时间;待处理交易为nil
transactionAmount
CurrencyAmount
始终为正
creditDebitIndicator
CreditDebitIndicator
.debit
.credit
transactionDescription
String
适合展示的描述
originalTransactionDescription
String
机构原始描述
merchantName
String?
商户名称(若可用)
merchantCategoryCode
MerchantCategoryCode?
ISO 18245编码
transactionType
TransactionType
.pointOfSale
.transfer
status
TransactionStatus
.authorized
.pending
.booked
.memo
.rejected
foreignCurrencyAmount
CurrencyAmount?
外币金额(若适用)
foreignCurrencyExchangeRate
Decimal?
汇率(若适用)

Long-Running Queries and History

长时查询与历史记录

Use
AsyncSequence
-based history APIs for live updates or resumable sync. These return
FinanceStore.Changes
(inserted, updated, deleted items) plus a
HistoryToken
for resumption.
swift
func monitorTransactions(for accountID: UUID) async throws {
    let history = store.transactionHistory(
        forAccountID: accountID,
        since: loadSavedToken(),
        isMonitoring: true  // true = keep streaming; false = terminate after catch-up
    )

    for try await changes in history {
        // changes.inserted, changes.updated, changes.deleted
        saveToken(changes.newToken)
    }
}
使用基于
AsyncSequence
的历史记录API实现实时更新或可恢复同步。这些API返回
FinanceStore.Changes
(插入、更新、删除的项目)以及用于恢复查询的
HistoryToken
swift
func monitorTransactions(for accountID: UUID) async throws {
    let history = store.transactionHistory(
        forAccountID: accountID,
        since: loadSavedToken(),
        isMonitoring: true  // true = 持续推送;false = 追上历史数据后终止
    )

    for try await changes in history {
        // changes.inserted、changes.updated、changes.deleted
        saveToken(changes.newToken)
    }
}

History Token Persistence

历史记录Token持久化

HistoryToken
conforms to
Codable
. Persist it to resume queries without reprocessing data:
swift
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)
}
If a saved token points to compacted history, the framework throws
FinanceError.historyTokenInvalid
. Discard the token and start fresh.
HistoryToken
遵循
Codable
协议。持久化该Token可以恢复查询,无需重新处理数据:
swift
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)
}
如果保存的Token指向已压缩的历史记录,框架会抛出
FinanceError.historyTokenInvalid
错误。请丢弃该Token并重新开始查询。

Account and Balance History

账户与余额历史记录

swift
let accountChanges = store.accountHistory(since: nil, isMonitoring: true)
let balanceChanges = store.accountBalanceHistory(forAccountID: accountID, since: nil, isMonitoring: true)
swift
let accountChanges = store.accountHistory(since: nil, isMonitoring: true)
let balanceChanges = store.accountBalanceHistory(forAccountID: accountID, since: nil, isMonitoring: true)

Transaction Picker

交易选择器

For apps that need selective, ephemeral access without full authorization, use
TransactionPicker
from FinanceKitUI. Access is not persisted -- transactions are passed directly for immediate use.
swift
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")
            }
        }
    }
}
对于需要选择性、临时访问且无需完整授权的应用,可以使用FinanceKitUI提供的
TransactionPicker
。访问权限不会持久化——交易将直接传递给应用供即时使用。
swift
import FinanceKitUI

struct ExpenseImportView: View {
    @State private var selectedTransactions: [Transaction] = []

    var body: some View {
        if FinanceStore.isDataAvailable(.financialData) {
            TransactionPicker(selection: $selectedTransactions) {
                Label("导入交易记录", systemImage: "creditcard")
            }
        }
    }
}

Wallet Orders

Wallet订单

FinanceKit supports saving and querying Wallet orders (e.g., purchase receipts, shipping tracking).
FinanceKit支持保存和查询Wallet订单(例如购买收据、物流跟踪)。

Saving an Order

保存订单

swift
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
}
swift
let result = try await store.saveOrder(signedArchive: archiveData)
switch result {
case .added:        break  // 已保存
case .cancelled:    break  // 用户取消
case .newerExisting: break // Wallet中已有更新版本
@unknown default:   break
}

Checking for an Existing Order

检查是否存在已有订单

swift
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
swift
let orderID = FullyQualifiedOrderIdentifier(
    orderTypeIdentifier: "com.merchant.order",
    orderIdentifier: "ORDER-123"
)
let result = try await store.containsOrder(matching: orderID, updatedDate: lastKnownDate)
// result: .exists、.newerExists、.olderExists 或 .notFound

Add Order to Wallet Button (FinanceKitUI)

添加订单到Wallet按钮(FinanceKitUI)

swift
import FinanceKitUI

AddOrderToWalletButton(signedArchive: orderData) { result in
    // result: .success(SaveOrderResult) or .failure(Error)
}
swift
import FinanceKitUI

AddOrderToWalletButton(signedArchive: orderData) { result in
    // result: .success(SaveOrderResult) 或 .failure(Error)
}

Background Delivery

后台推送

iOS 26+ supports background delivery extensions that notify your app of financial data changes outside its lifecycle. Requires App Groups to share data between the app and extension.
iOS 26及以上版本支持后台推送扩展,可在应用生命周期外通知应用财务数据变更。需要使用App Groups在应用和扩展间共享数据。

Enabling Background Delivery

启用后台推送

swift
try await store.enableBackgroundDelivery(
    for: [.transactions, .accountBalances],
    frequency: .daily
)
Available frequencies:
.hourly
,
.daily
,
.weekly
.
Disable selectively or entirely:
swift
try await store.disableBackgroundDelivery(for: [.transactions])
try await store.disableAllBackgroundDelivery()
swift
try await store.enableBackgroundDelivery(
    for: [.transactions, .accountBalances],
    frequency: .daily
)
可用频率:
.hourly
(每小时)、
.daily
(每天)、
.weekly
(每周)。
禁用选择性或完全禁用后台推送:
swift
try await store.disableBackgroundDelivery(for: [.transactions])
try await store.disableAllBackgroundDelivery()

Background Delivery Extension

后台推送扩展

Create a background delivery extension target in Xcode (Background Delivery Extension template). Both the app and extension must belong to the same App Group.
swift
import FinanceKit

struct MyFinanceExtension: BackgroundDeliveryExtension {
    var body: some BackgroundDeliveryExtensionProviding { FinanceDataHandler() }
}

struct FinanceDataHandler: BackgroundDeliveryExtensionProviding {
    func didReceiveData(for dataTypes: [FinanceStore.BackgroundDataType]) async {
        for dataType in dataTypes {
            switch dataType {
            case .transactions:    await processNewTransactions()
            case .accountBalances: await updateBalanceCache()
            case .accounts:        await refreshAccountList()
            @unknown default:      break
            }
        }
    }

    func willTerminate() async { /* Clean up */ }
}
在Xcode中创建后台推送扩展目标(后台推送扩展模板)。应用和扩展必须属于同一个App Group。
swift
import FinanceKit

struct MyFinanceExtension: BackgroundDeliveryExtension {
    var body: some BackgroundDeliveryExtensionProviding { FinanceDataHandler() }
}

struct FinanceDataHandler: BackgroundDeliveryExtensionProviding {
    func didReceiveData(for dataTypes: [FinanceStore.BackgroundDataType]) async {
        for dataType in dataTypes {
            switch dataType {
            case .transactions:    await processNewTransactions()
            case .accountBalances: await updateBalanceCache()
            case .accounts:        await refreshAccountList()
            @unknown default:      break
            }
        }
    }

    func willTerminate() async { /* 清理资源 */ }
}

Common Mistakes

常见错误

1. Calling APIs when data is unavailable

1. 数据不可用时调用API

DON'T -- skip availability check:
swift
let store = FinanceStore.shared
let status = try await store.requestAuthorization() // Terminates if unavailable
DO -- guard availability first:
swift
guard FinanceStore.isDataAvailable(.financialData) else {
    showUnavailableMessage()
    return
}
let status = try await FinanceStore.shared.requestAuthorization()
错误做法——跳过可用性检查:
swift
let store = FinanceStore.shared
let status = try await store.requestAuthorization() // 不可用时会终止应用
正确做法——先判断可用性:
swift
guard FinanceStore.isDataAvailable(.financialData) else {
    showUnavailableMessage()
    return
}
let status = try await FinanceStore.shared.requestAuthorization()

2. Ignoring the credit/debit indicator

2. 忽略借贷指示器

DON'T -- treat amounts as signed values:
swift
let spent = transaction.transactionAmount.amount // Always positive
DO -- apply the indicator:
swift
let amount = transaction.transactionAmount.amount
let signed = transaction.creditDebitIndicator == .debit ? -amount : amount
错误做法——将金额视为有符号值:
swift
let spent = transaction.transactionAmount.amount // 始终为正
正确做法——应用指示器判断符号:
swift
let amount = transaction.transactionAmount.amount
let signed = transaction.creditDebitIndicator == .debit ? -amount : amount

3. Not handling data restriction errors

3. 未处理数据受限错误

DON'T -- assume authorized access persists:
swift
let transactions = try await store.transactions(query: query) // Fails if Wallet restricted
DO -- catch
FinanceError
:
swift
do {
    let transactions = try await store.transactions(query: query)
} catch let error as FinanceError {
    if case .dataRestricted = error { showDataRestrictedMessage() }
}
错误做法——假设授权访问会一直有效:
swift
let transactions = try await store.transactions(query: query) // Wallet受限时会失败
正确做法——捕获
FinanceError
swift
do {
    let transactions = try await store.transactions(query: query)
} catch let error as FinanceError {
    if case .dataRestricted = error { showDataRestrictedMessage() }
}

4. Requesting full snapshots instead of resumable queries

4. 请求全量快照而非可恢复查询

DON'T -- fetch everything on every launch:
swift
let allTransactions = try await store.transactions(query: TransactionQuery(
    sortDescriptors: [SortDescriptor(\Transaction.transactionDate)],
    predicate: nil, limit: nil, offset: nil
))
DO -- use history tokens for incremental sync:
swift
let history = store.transactionHistory(
    forAccountID: accountID,
    since: loadSavedToken(),
    isMonitoring: false
)
for try await changes in history {
    processChanges(changes)
    saveToken(changes.newToken)
}
错误做法——每次启动都拉取所有数据:
swift
let allTransactions = try await store.transactions(query: TransactionQuery(
    sortDescriptors: [SortDescriptor(\Transaction.transactionDate)],
    predicate: nil, limit: nil, offset: nil
))
正确做法——使用历史记录Token进行增量同步:
swift
let history = store.transactionHistory(
    forAccountID: accountID,
    since: loadSavedToken(),
    isMonitoring: false
)
for try await changes in history {
    processChanges(changes)
    saveToken(changes.newToken)
}

5. Not persisting history tokens

5. 未持久化历史记录Token

DON'T -- discard the token:
swift
for try await changes in history {
    processChanges(changes)
    // Token lost -- next launch reprocesses everything
}
DO -- save every token:
swift
for try await changes in history {
    processChanges(changes)
    saveToken(changes.newToken)
}
错误做法——丢弃Token:
swift
for try await changes in history {
    processChanges(changes)
    // Token丢失——下次启动会重新处理所有数据
}
正确做法——保存每次返回的Token:
swift
for try await changes in history {
    processChanges(changes)
    saveToken(changes.newToken)
}

6. Misinterpreting credit/debit on liability accounts

6. 误解负债账户的借贷标记

Both asset and liability accounts use
.debit
for outgoing money. But
.credit
means different things: on an asset account it means money received; on a liability account it means a payment or refund that increases available credit. See references/financekit-patterns.md for a full interpretation table.
资产和负债账户都使用
.debit
表示流出资金。但
.credit
的含义不同:在资产账户中表示资金入账;在负债账户中表示还款或退款,会增加可用信用额度。完整的解释表请查看references/financekit-patterns.md

Review Checklist

审核清单

  • FinanceStore.isDataAvailable(.financialData)
    checked before any API call
  • com.apple.developer.financekit
    entitlement requested and approved by Apple
  • NSFinancialDataUsageDescription
    set in Info.plist with a clear, specific message
  • Organization-level Apple Developer account used
  • Authorization status handled for all cases (
    .authorized
    ,
    .denied
    ,
    .notDetermined
    )
  • FinanceError.dataRestricted
    caught and handled gracefully
  • CreditDebitIndicator
    applied correctly to amounts (not treated as signed)
  • History tokens persisted for resumable queries
  • FinanceError.historyTokenInvalid
    handled by discarding token and restarting
  • Long-running queries use
    isMonitoring: false
    when live updates are not needed
  • Transaction picker used when full authorization is unnecessary
  • Only data the app genuinely needs is queried
  • Deleted data from history changes is removed from local storage
  • Background delivery extension in same App Group as the main app (iOS 26+)
  • Financial data deleted when user revokes access
  • 调用任何API前已检查
    FinanceStore.isDataAvailable(.financialData)
  • 已申请并获得苹果批准的
    com.apple.developer.financekit
    权限
  • Info.plist中已设置
    NSFinancialDataUsageDescription
    ,且信息清晰具体
  • 使用的是组织级Apple Developer账户
  • 已处理所有授权状态情况(
    .authorized
    .denied
    .notDetermined
  • 已捕获并妥善处理
    FinanceError.dataRestricted
    错误
  • 已正确对金额应用
    CreditDebitIndicator
    (未直接视为有符号值)
  • 已持久化历史记录Token用于可恢复查询
  • 已处理
    FinanceError.historyTokenInvalid
    错误,方式为丢弃Token并重启查询
  • 不需要实时更新时,长时查询设置了
    isMonitoring: false
  • 不需要完整授权时使用了交易选择器
  • 仅查询应用真实需要的数据
  • 历史记录变更中的已删除数据已从本地存储中移除
  • 后台推送扩展与主应用属于同一个App Group(iOS 26及以上)
  • 用户撤销访问时已删除相关财务数据

References

参考资料