Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
90
90%
Does it follow best practices?
Impact
—
Average score across 248 eval scenarios
Advisory
Suggest reviewing before use
Access eligible financial data from Apple Wallet, including U.S. Apple Card, Apple Cash, Savings, and U.K. connected-account data. FinanceKit provides on-device access to accounts, balances, and transactions with user-controlled authorization. Targets Swift 6.3 / current Apple platforms; query APIs are available from iOS/iPadOS 17.4, TransactionPicker from iOS/iPadOS 18, and background delivery from iOS/iPadOS 26.
Keep FinanceKit guidance focused on financial-data access, Wallet order storage/querying, TransactionPicker, and background delivery. Route Apple Pay checkout to PassKit, widget UI/timeline work to WidgetKit, and Wallet order-tracking email or Apple Business Connect optimization outside this skill.
com.apple.developer.financekit from Apple via the FinanceKit entitlement request form. This is a managed capability; Apple reviews each application.NSFinancialDataUsageDescription to Info.plist -- this string is shown to the user during the authorization prompt.<key>NSFinancialDataUsageDescription</key>
<string>This app uses your financial data to track spending and provide budgeting insights.</string>U.S. FinanceKit financial data requires iOS/iPadOS 17.4+ and currently covers eligible Apple Card, Apple Cash, and Savings data; Apple Card Family participants and Apple Cash Family children are excluded. U.K. support requires iOS/iPadOS 18.4+ and uses open banking for supported institutions. Orders APIs are available separately from financial-data query APIs.
Check whether the device supports FinanceKit before making any API calls. This value is constant across launches and iOS versions.
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:
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.
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.
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
}Query current authorization without prompting:
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.
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.
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)")
}
}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.
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)
}Amounts are always positive decimals. Use creditDebitIndicator to determine the sign:
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"
}Use TransactionQuery with Swift predicates, sort descriptors, limit, and offset.
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 optionalFinanceKit provides factory methods for common filters:
// 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
])For a transaction field table and more query patterns, read references/financekit-patterns.md.
Use AsyncSequence-based history APIs for catch-up sync, live updates, or resumable sync. These return inserted, updated, and deleted item IDs plus a 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 conforms to Codable. Persist it to resume queries without reprocessing data:
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, then immediately run a fresh catch-up query for the affected account or balance stream so local state and the replacement token are rebuilt. Use isMonitoring: true only for a separate live monitor.
let accountChanges = store.accountHistory(since: nil, isMonitoring: true)
let balanceChanges = store.accountBalanceHistory(forAccountID: accountID, since: nil, isMonitoring: true)Ongoing budgeting sync should cover the data model the user authorized: account objects for account additions/removals, account balances for trend and widget state, and transactions for spending detail. Use separate history tokens per stream or account so a compacted token only forces resync of the affected stream.
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.
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")
}
}
}
}FinanceKit supports saving and querying Wallet orders (e.g., purchase receipts, shipping tracking).
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 .notFoundimport FinanceKitUI
AddOrderToWalletButton(signedArchive: orderData) { result in
// result: .success(SaveOrderResult) or .failure(Error)
}iOS 26+ supports background delivery extensions that notify your app of financial data changes outside its lifecycle. User authorization made in the main app is inherited by the extension. Both targets need the FinanceKit entitlement; use App Groups to share data between the app, extension, and related widgets.
These registration methods are synchronous and nonthrowing; do not write try or await.
store.enableBackgroundDelivery(
for: [.accounts, .accountBalances, .transactions],
frequency: .daily
)Available frequencies: .hourly, .daily, .weekly. These are expected minimum intervals between extension launches when data changes; longer frequencies give the extension a larger processing window.
Disable selectively or entirely:
store.disableBackgroundDelivery(for: [.transactions])
store.disableAllBackgroundDelivery()Create a background delivery extension target in Xcode (Background Delivery Extension template). Implement the two async entry points directly on the extension type and return from didReceiveData(for:) only after essential work is saved.
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() }
}DON'T -- skip availability check:
let store = FinanceStore.shared
let status = try await store.requestAuthorization() // Terminates if unavailableDO -- guard availability first:
guard FinanceStore.isDataAvailable(.financialData) else {
showUnavailableMessage()
return
}
let status = try await FinanceStore.shared.requestAuthorization()DON'T -- treat amounts as signed values:
let spent = transaction.transactionAmount.amount // Always positiveDO -- apply the indicator:
let amount = transaction.transactionAmount.amount
let signed = transaction.creditDebitIndicator == .debit ? -amount : amountDON'T -- assume authorized access persists:
let transactions = try await store.transactions(query: query) // Fails if Wallet restrictedDO -- catch FinanceError:
do {
let transactions = try await store.transactions(query: query)
} catch let error as FinanceError {
if case .dataRestricted = error { showDataRestrictedMessage() }
}DON'T -- fetch everything on every launch:
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:
let history = store.transactionHistory(
forAccountID: accountID,
since: loadSavedToken(),
isMonitoring: false
)
for try await changes in history {
removeLocalRecords(withIDs: changes.deleted)
upsert(changes.inserted + changes.updated)
saveToken(changes.newToken)
}DON'T -- discard the token:
for try await changes in history {
processChanges(changes)
// Token lost -- next launch reprocesses everything
}DO -- save every token:
for try await changes in history {
removeLocalRecords(withIDs: changes.deleted)
upsert(changes.inserted + changes.updated)
saveToken(changes.newToken)
}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.
FinanceStore.isDataAvailable(.financialData) checked before any API callcom.apple.developer.financekit entitlement requested and approved for the app bundle IDNSFinancialDataUsageDescription set in Info.plist with a clear, specific message.authorized, .denied, .notDetermined)FinanceError.dataRestricted caught and handled gracefullyCreditDebitIndicator applied correctly to amounts (not treated as signed)FinanceError.historyTokenInvalid handled by discarding token and immediately resyncing the affected streamisMonitoring: false when live updates are not needed.accounts, .accountBalances, and/or .transactions.tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references