Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Overflow reference for the financekit skill. Contains advanced query patterns, currency handling, and background delivery details that exceed the main skill file's scope.
FinanceKit queries accept Swift #Predicate macros. Combine conditions directly within the predicate.
import FinanceKit
func fetchRecentDebits(
for accountID: UUID,
since date: Date
) async throws -> [Transaction] {
let store = FinanceStore.shared
let predicate = #Predicate<Transaction> { transaction in
transaction.accountID == accountID &&
transaction.transactionDate > date &&
transaction.creditDebitIndicator == .debit
}
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate,
limit: nil,
offset: nil
)
return try await store.transactions(query: query)
}FinanceKit provides static factory methods on query types for common patterns:
// Transactions by status
let bookedPredicate = TransactionQuery.predicate(forStatuses: [.booked])
// Transactions by type
let purchasePredicate = TransactionQuery.predicate(
forTransactionTypes: [.pointOfSale, .directDebit, .billPayment]
)
// Transactions by merchant category code
let diningPredicate = TransactionQuery.predicate(
forMerchantCategoryCodes: [
MerchantCategoryCode(rawValue: 5812), // Restaurants
MerchantCategoryCode(rawValue: 5814), // Fast food
]
)
// Balances by date range (available balance)
let balancePredicate = AccountBalanceQuery.predicate(
availableSince: startDate,
until: endDate
)
// Balances by date range (booked balance)
let bookedBalancePredicate = AccountBalanceQuery.predicate(
bookedSince: startDate,
until: endDate
)func fetchTransactionsInRange(
accountID: UUID,
from startDate: Date,
to endDate: Date
) async throws -> [Transaction] {
let predicate = #Predicate<Transaction> { transaction in
transaction.accountID == accountID &&
transaction.transactionDate >= startDate &&
transaction.transactionDate <= endDate
}
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate,
limit: nil,
offset: nil
)
return try await FinanceStore.shared.transactions(query: query)
}Some transactions have a postedDate (when booked by the institution) distinct from transactionDate:
let predicate = #Predicate<Transaction> { transaction in
transaction.postedDate != nil &&
transaction.status == .booked
}let query = TransactionQuery(
sortDescriptors: [
SortDescriptor(\Transaction.transactionDate, order: .reverse),
SortDescriptor(\Transaction.transactionDescription)
],
predicate: nil,
limit: 20,
offset: nil
)Use limit and offset for paged access:
@Observable
@MainActor
final class TransactionPager {
private let store = FinanceStore.shared
private let pageSize = 25
private var currentOffset = 0
private(set) var transactions: [Transaction] = []
private(set) var hasMore = true
let accountID: UUID
init(accountID: UUID) {
self.accountID = accountID
}
func loadNextPage() async throws {
guard hasMore else { return }
let predicate = #Predicate<Transaction> { transaction in
transaction.accountID == self.accountID
}
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate,
limit: pageSize,
offset: currentOffset
)
let page = try await store.transactions(query: query)
transactions.append(contentsOf: page)
currentOffset += page.count
hasMore = page.count == pageSize
}
func reset() {
transactions = []
currentOffset = 0
hasMore = true
}
}let accountQuery = AccountQuery(
sortDescriptors: [
SortDescriptor(\Account.institutionName),
SortDescriptor(\Account.displayName)
],
predicate: nil,
limit: nil,
offset: nil
)MerchantCategoryCode wraps an Int16 raw value conforming to ISO 18245. Common codes:
| Code | Category |
|---|---|
| 5411 | Grocery stores |
| 5541 | Gas stations |
| 5812 | Restaurants |
| 5814 | Fast food |
| 5912 | Pharmacies |
| 5999 | Miscellaneous retail |
| 7011 | Hotels and motels |
| 7832 | Movie theaters |
| 4121 | Rideshare / taxis |
| 5311 | Department stores |
func groupByCategory(_ transactions: [Transaction]) -> [Int16: [Transaction]] {
var groups: [Int16: [Transaction]] = [:]
for transaction in transactions {
let code = transaction.merchantCategoryCode?.rawValue ?? -1
groups[code, default: []].append(transaction)
}
return groups
}MerchantCategoryCode conforms to CustomStringConvertible, providing a description property for display:
if let mcc = transaction.merchantCategoryCode {
print("Category: \(mcc.description)")
}FinanceKit stores amounts as CurrencyAmount with a Decimal amount and a currency code string. Use FormatStyle for localized display.
func formatCurrency(_ amount: CurrencyAmount) -> String {
amount.amount.formatted(
.currency(code: amount.currencyCode)
)
}Amounts are always positive. Apply sign based on creditDebitIndicator:
func formatSignedAmount(
_ amount: CurrencyAmount,
indicator: CreditDebitIndicator,
accountType: Account
) -> String {
var value = amount.amount
switch accountType {
case .asset:
if indicator == .debit { value = -value }
case .liability:
if indicator == .debit { value = -value }
}
return value.formatted(.currency(code: amount.currencyCode))
}func displayForeignTransaction(_ transaction: Transaction) -> String {
var result = formatCurrency(transaction.transactionAmount)
if let foreign = transaction.foreignCurrencyAmount {
result += " (originally \(formatCurrency(foreign))"
if let rate = transaction.foreignCurrencyExchangeRate {
result += " at rate \(rate)"
}
result += ")"
}
return result
}Transactions progress through statuses as they are processed by the institution.
| Status | Meaning |
|---|---|
.authorized | Transaction approved but not yet processed |
.pending | Processing by the institution |
.memo | Informational entry, not yet settled |
.booked | Fully settled and posted |
.rejected | Declined by the institution |
func fetchPendingTransactions(for accountID: UUID) async throws -> [Transaction] {
let predicate = #Predicate<Transaction> { transaction in
transaction.accountID == accountID &&
(transaction.status == .pending || transaction.status == .authorized)
}
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate,
limit: nil,
offset: nil
)
return try await FinanceStore.shared.transactions(query: query)
}func statusLabel(for status: TransactionStatus) -> String {
switch status {
case .authorized: "Authorized"
case .pending: "Pending"
case .memo: "Memo"
case .booked: "Posted"
case .rejected: "Declined"
@unknown default: "Unknown"
}
}Use paginated balance queries to build historical balance charts.
func fetchBalanceHistory(
for accountID: UUID,
limit: Int = 30
) async throws -> [AccountBalance] {
let predicate = #Predicate<AccountBalance> { balance in
balance.accountID == accountID
}
let query = AccountBalanceQuery(
sortDescriptors: [SortDescriptor(\AccountBalance.id)],
predicate: predicate,
limit: limit,
offset: nil
)
return try await FinanceStore.shared.accountBalances(query: query)
}Use the built-in predicate factories:
let thirtyDaysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
let query = AccountBalanceQuery(
sortDescriptors: [SortDescriptor(\AccountBalance.id)],
predicate: AccountBalanceQuery.predicate(
availableSince: thirtyDaysAgo,
until: nil
),
limit: nil,
offset: nil
)struct BalanceDataPoint: Identifiable {
let id: UUID
let date: Date
let amount: Decimal
let currencyCode: String
}
func balanceChartData(from balances: [AccountBalance]) -> [BalanceDataPoint] {
balances.compactMap { balance in
switch balance.currentBalance {
case .available(let bal), .booked(let bal):
let signed = bal.creditDebitIndicator == .credit ? bal.amount.amount : -bal.amount.amount
return BalanceDataPoint(
id: balance.id,
date: bal.asOfDate,
amount: signed,
currencyCode: bal.currencyCode
)
case .availableAndBooked(let available, _):
let signed = available.creditDebitIndicator == .credit
? available.amount.amount : -available.amount.amount
return BalanceDataPoint(
id: balance.id,
date: available.asOfDate,
amount: signed,
currencyCode: balance.currencyCode
)
@unknown default:
return nil
}
}
}The meaning of CreditDebitIndicator varies by account type. This is a common source of confusion.
| Indicator | Balance Effect | Example |
|---|---|---|
.debit | Decreases balance | Sending money via Apple Cash |
.credit | Increases balance | Receiving a payment |
| Indicator | Balance Effect | Example |
|---|---|---|
.debit | Decreases available credit | Making a purchase |
.credit | Increases available credit | Payment or refund |
enum MoneyDirection {
case incoming, outgoing
}
func direction(
of transaction: Transaction,
in account: Account
) -> MoneyDirection {
// For both asset and liability accounts, debit represents money going out
// (balance decrease for assets, credit decrease for liabilities)
transaction.creditDebitIndicator == .debit ? .outgoing : .incoming
}A complete manager for incremental sync with token persistence and error recovery.
import FinanceKit
@Observable
@MainActor
final class FinanceSyncManager {
private let store = FinanceStore.shared
private let tokenKey = "financekit.sync.token"
private(set) var accounts: [Account] = []
private(set) var transactions: [UUID: [Transaction]] = [:]
private(set) var syncError: Error?
// MARK: - Initial Load
func performInitialLoad() async {
guard FinanceStore.isDataAvailable(.financialData) else { return }
do {
let status = try await store.authorizationStatus()
guard status == .authorized else { return }
accounts = try await fetchAllAccounts()
} catch {
syncError = error
}
}
// MARK: - Incremental Sync
func syncTransactions(for accountID: UUID) async {
let token = loadToken(for: accountID)
do {
let history = store.transactionHistory(
forAccountID: accountID,
since: token,
isMonitoring: false
)
for try await changes in history {
applyChanges(changes, for: accountID)
saveToken(changes.newToken, for: accountID)
}
} catch let error as FinanceError where error == .historyTokenInvalid {
// Token expired -- full resync
clearToken(for: accountID)
await syncTransactions(for: accountID)
} catch {
syncError = error
}
}
// MARK: - Live Monitoring
func startMonitoring(for accountID: UUID) async {
let token = loadToken(for: accountID)
do {
let history = store.transactionHistory(
forAccountID: accountID,
since: token,
isMonitoring: true
)
for try await changes in history {
applyChanges(changes, for: accountID)
saveToken(changes.newToken, for: accountID)
}
} catch {
syncError = error
}
}
// MARK: - Private
private func fetchAllAccounts() async throws -> [Account] {
let query = AccountQuery(
sortDescriptors: [SortDescriptor(\Account.displayName)],
predicate: nil,
limit: nil,
offset: nil
)
return try await store.accounts(query: query)
}
private func applyChanges(
_ changes: FinanceStore.Changes<Transaction>,
for accountID: UUID
) {
var current = transactions[accountID] ?? []
// Remove deleted
let deletedSet = Set(changes.deleted)
current.removeAll { deletedSet.contains($0.id) }
// Update existing
for updated in changes.updated {
if let index = current.firstIndex(where: { $0.id == updated.id }) {
current[index] = updated
}
}
// Insert new
current.append(contentsOf: changes.inserted)
// Sort by date descending
current.sort { $0.transactionDate > $1.transactionDate }
transactions[accountID] = current
}
private func saveToken(_ token: FinanceStore.HistoryToken, for accountID: UUID) {
let key = "\(tokenKey).\(accountID.uuidString)"
if let data = try? JSONEncoder().encode(token) {
UserDefaults.standard.set(data, forKey: key)
}
}
private func loadToken(for accountID: UUID) -> FinanceStore.HistoryToken? {
let key = "\(tokenKey).\(accountID.uuidString)"
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return try? JSONDecoder().decode(FinanceStore.HistoryToken.self, from: data)
}
private func clearToken(for accountID: UUID) {
let key = "\(tokenKey).\(accountID.uuidString)"
UserDefaults.standard.removeObject(forKey: key)
}
}import SwiftUI
import FinanceKit
struct AccountListView: View {
@State private var accounts: [Account] = []
var body: some View {
NavigationStack {
List(accounts, id: \.id) { account in
NavigationLink(value: account.id) {
VStack(alignment: .leading) {
Text(account.displayName).font(.headline)
Text(account.institutionName).font(.subheadline).foregroundStyle(.secondary)
}
}
}
.navigationTitle("Accounts")
.navigationDestination(for: UUID.self) { TransactionListView(accountID: $0) }
.task { await loadAccounts() }
}
}
private func loadAccounts() async {
guard FinanceStore.isDataAvailable(.financialData) else { return }
do {
let status = try await FinanceStore.shared.requestAuthorization()
guard status == .authorized else { return }
let query = AccountQuery(
sortDescriptors: [SortDescriptor(\Account.displayName)],
predicate: nil, limit: nil, offset: nil
)
accounts = try await FinanceStore.shared.accounts(query: query)
} catch { }
}
}struct TransactionListView: View {
let accountID: UUID
@State private var transactions: [Transaction] = []
var body: some View {
List(transactions, id: \.id) { transaction in
HStack {
VStack(alignment: .leading) {
Text(transaction.transactionDescription)
if let merchant = transaction.merchantName {
Text(merchant).font(.caption).foregroundStyle(.secondary)
}
}
Spacer()
VStack(alignment: .trailing) {
let amount = transaction.transactionAmount
let sign = transaction.creditDebitIndicator == .debit ? "-" : "+"
Text("\(sign)\(amount.amount.formatted(.currency(code: amount.currencyCode)))")
.font(.body.monospacedDigit())
Text(transaction.transactionDate, style: .date)
.font(.caption).foregroundStyle(.secondary)
}
}
}
.navigationTitle("Transactions")
.task {
let predicate = #Predicate<Transaction> { $0.accountID == accountID }
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate, limit: 100, offset: nil
)
transactions = (try? await FinanceStore.shared.transactions(query: query)) ?? []
}
}
}The background delivery extension requires:
Use a shared container for data accessible to both the app and extension:
let sharedDefaults = UserDefaults(suiteName: "group.com.myapp.finance")
// In extension: sync latest data to shared container
func processNewTransactions() async {
let store = FinanceStore.shared
for account in try await fetchAccounts() {
let history = store.transactionHistory(
forAccountID: account.id, since: loadSharedToken(), isMonitoring: false
)
for try await changes in history {
persistToSharedStore(changes)
saveSharedToken(changes.newToken)
}
}
}didReceiveData(for:) is called when the system detects changes matching the registered data types.willTerminate() provides a cleanup opportunity before the system terminates the extension.do {
let transactions = try await store.transactions(query: query)
} catch let error as FinanceError {
switch error {
case .dataRestricted(let dataType):
handleRestriction(dataType) // Wallet unavailable or MDM restricted
case .historyTokenInvalid:
discardSavedToken() // Token points to compacted history
case .unknown:
logError(error)
@unknown default:
logError(error)
}
}@Observable
@MainActor
final class FinanceDataProvider {
enum State {
case loading, available([Transaction]), unavailable(reason: String)
}
private(set) var state: State = .loading
func load(accountID: UUID) async {
guard FinanceStore.isDataAvailable(.financialData) else {
state = .unavailable(reason: "Financial data is not available on this device.")
return
}
do {
let status = try await FinanceStore.shared.authorizationStatus()
guard status == .authorized else {
state = .unavailable(reason: "Access to financial data has not been granted.")
return
}
let predicate = #Predicate<Transaction> { $0.accountID == accountID }
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate, limit: 50, offset: nil
)
state = .available(try await FinanceStore.shared.transactions(query: query))
} catch let error as FinanceError {
state = .unavailable(reason: error == .dataRestricted(.financialData)
? "Financial data is temporarily restricted."
: "Unable to load financial data.")
} catch {
state = .unavailable(reason: "An unexpected error occurred.")
}
}
}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
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