Apple provides a framework for nearly every device feature, and job posts list the names: StoreKit, Vision, AVFoundation, HealthKit. You will not master them all, and you do not need to. What you need is to know what each one is for, to be able to start one using the documentation, and to know the rules that come with it. This lesson gives a working first step in each of four of the most requested ones.
StoreKit 2: in-app purchases
StoreKit lets you sell things inside your app: a one-time unlock, a consumable such as credits, or an auto-renewing subscription. You create the products in App Store Connect with a product id, a price tier and a description. In the app, you fetch them, show them and start the purchase. StoreKit 2 is the Swift-first version that uses async/await.
import StoreKit
@MainActor
@Observable
final class Store {
private(set) var products: [Product] = []
private(set) var isPro = false
private let ids = ["com.example.expensetracker.pro"]
func loadProducts() async {
do { products = try await Product.products(for: ids) }
catch { print("Could not load products:", error) }
}
func buy(_ product: Product) async {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
isPro = true
await transaction.finish()
}
case .userCancelled, .pending:
break
@unknown default:
break
}
} catch {
print("Purchase failed:", error)
}
}
func refreshEntitlements() async {
var active = false
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result, ids.contains(transaction.productID) {
active = true
}
}
isPro = active
}
}
purchase()returns a result. Only treat a purchase as real when itsverificationis.verified. StoreKit 2 checks Apple's signature for you.- Call
transaction.finish()after you have given the person what they bought. Transaction.currentEntitlementslists what the person owns now. Check it at launch, so a purchase made on another device, or a refund, is reflected.- Also listen to
Transaction.updatesin a long-running task, to catch purchases that finish later, for example after a *Ask to Buy* approval. - Test with a StoreKit configuration file in Xcode, which pretends to be the store, so you can test purchases, refunds and renewals with no real money. Then test with a Sandbox account before release.
Vision: reading what is in an image
Vision analyses images on the device: text, faces, barcodes, body poses and more. Nothing needs to be sent to a server, which is good for privacy and works offline. Here is text recognition, the *scan a receipt* feature that fits our Expense Tracker.
import Vision
import UIKit
func recognizeText(in image: UIImage) async throws -> [String] {
guard let cgImage = image.cgImage else { return [] }
return try await withCheckedThrowingContinuation { continuation in
let request = VNRecognizeTextRequest { request, error in
if let error { continuation.resume(throwing: error); return }
let observations = request.results as? [VNRecognizedTextObservation] ?? []
let lines = observations.compactMap { $0.topCandidates(1).first?.string }
continuation.resume(returning: lines)
}
request.recognitionLevel = .accurate
DispatchQueue.global(qos: .userInitiated).async {
let handler = VNImageRequestHandler(cgImage: cgImage)
do { try handler.perform([request]) }
catch { continuation.resume(throwing: error) }
}
}
}
This also shows a useful pattern. Vision uses a completion handler, an older style, so we wrap it in withCheckedThrowingContinuation to make it async. You will do the same with many older Apple APIs. The continuation must be resumed exactly once. The result is a list of text lines. Finding the *total* in a receipt from those lines is your own logic, and you should always let the user check and correct what was read.
AVFoundation: audio, video and the camera
AVFoundation is the large framework for media. At the simplest level, playing a video in SwiftUI takes a few lines with AVKit.
import AVKit
import SwiftUI
struct PlayerScreen: View {
let url: URL
@State private var player: AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear { player = AVPlayer(url: url) }
.onDisappear { player?.pause() }
}
}
- Playing sound:
AVAudioPlayerfor short files,AVPlayerfor streams and video. - Audio sessions:
AVAudioSessiontells the system whether your app should play over other music, mute with the silent switch, or record. - The camera:
AVCaptureSessionwith an input and an output gives full control, and needsNSCameraUsageDescription. If you only need a photo, the system'sPhotosPickerand camera picker are much simpler, and need less permission.
HealthKit
HealthKit reads and writes health data that the person keeps on the device, such as steps, sleep and workouts. The rules are the strictest of the frameworks here. You add the HealthKit capability, write a usage description that says exactly why you read each type, and ask for each kind of data separately. The person may allow some and not others, and your app cannot even tell whether *reading* was denied. Apple's guidelines restrict how health data may be used, for example it must not be used for advertising, so read them before you design a feature.
import HealthKit
func requestStepAccess() async throws {
guard HKHealthStore.isHealthDataAvailable() else { return }
let store = HKHealthStore()
let steps = HKQuantityType(.stepCount)
try await store.requestAuthorization(toShare: [], read: [steps])
}
How to learn a new framework
- Read the framework's overview page and look for a sample app from Apple.
- Add the capability and usage description first. Many first-run crashes are a missing description.
- Get the smallest result working, like one marker or one text line, before you design the screen.
- Wrap the framework in your own small type, as with
Storeabove, so the rest of the app does not depend on its details and you can fake it in tests.
Common mistakes
- Granting a purchase without checking
.verified. - A purchase flow with no *Restore Purchases* option.
- Resuming a continuation twice, or never. Both are bugs.
- Collecting health or location data you do not need.
Your task
- Create a StoreKit configuration file with one non-consumable product, run the
Storeclass and buy it in the Simulator. - Add a *Restore Purchases* button that calls
AppStore.sync()and then refreshes the entitlements. - Use
recognizeText(in:)on a photo of a printed receipt and list the lines in aList. - Write one sentence for each framework in this lesson: what it is for, and one rule that comes with it.
How to check it worked
- The purchase works in the configuration file and
isProstays on after relaunch. - The recognised lines appear, and you can correct them by hand.
- You can explain why a continuation must be resumed exactly once.