iOS with SwiftUI · Production: Architecture, Testing, Accessibility and the App Store
Accessibility, Performance and Shipping to the App Store
Make the app usable with VoiceOver and Dynamic Type, find performance problems with Instruments, and ship through TestFlight, App Store review and phased releases.
What you'll build
An accessible product row that VoiceOver reads as one clear element, a profiling pass that removes a scrolling hitch, and a release that goes from TestFlight testers to all users with a phased rollout.
The idea
Accessibility is a requirement, not a polish step:
- VoiceOver reads accessibility labels. Combine a row's parts with
.accessibilityElement(children: .combine)or give it an explicit.accessibilityLabel, and describe images that carry meaning. - Dynamic Type, sufficient contrast and touch targets of at least 44 by 44 points.
- Xcode's Accessibility Inspector audits screens automatically.
Performance: profile release builds with Instruments (Time Profiler, SwiftUI, Hangs, Allocations). Common fixes: move work out of body, use lazy stacks for long content, downsample images, and avoid re-rendering large views on unrelated changes.
Shipping:
- Archive in Xcode and upload to App Store Connect (or use Xcode Cloud).
- Test with TestFlight: internal testers immediately, external testers after a short beta review.
- Fill in the listing, privacy details ("nutrition labels") and a privacy manifest for your app and SDKs.
- Submit for App Review; follow the App Review Guidelines (sign-in with a demo account, no placeholder content).
- Release with a phased release over seven days, watching crashes in Xcode Organizer.
The code
struct ProductRow: View {
let product: Product
var body: some View {
HStack(spacing: 12) {
AsyncImage(url: product.thumbnailURL) { $0.resizable().scaledToFill() } placeholder: { Color.gray.opacity(0.1) }
.frame(width: 56, height: 56)
.clipShape(.rect(cornerRadius: 8))
.accessibilityHidden(true) // decorative
VStack(alignment: .leading) {
Text(product.title).font(.headline)
Text(product.priceLabel).font(.subheadline).foregroundStyle(.secondary)
}
Spacer()
if product.isOnSale {
Image(systemName: "tag.fill").foregroundStyle(.red)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityDescription(product))
.accessibilityAddTraits(.isButton)
.accessibilityIdentifier("product-\(product.id)")
}
}
Try the logic
What VoiceOver should say for a row is a small, testable function:
import Foundation
struct Product {
let id: String
let title: String
let cents: Int
let isOnSale: Bool
let rating: Double?
}
func accessibilityDescription(_ p: Product) -> String {
var parts = [p.title, "\(p.cents / 100) euros \(p.cents % 100) cents"]
if p.isOnSale { parts.append("on sale") }
if let rating = p.rating { parts.append("rated \(Int(rating.rounded())) out of 5") }
return parts.joined(separator: ", ")
}
let products = [
Product(id: "mug-01", title: "Stoneware mug", cents: 1299, isOnSale: true, rating: 4.6),
Product(id: "tea-02", title: "Green tea", cents: 650, isOnSale: false, rating: nil),
]
for p in products { print(accessibilityDescription(p)) }
Output
Stoneware mug, 12 euros 99 cents, on sale, rated 5 out of 5 Green tea, 6 euros 50 cents
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Unlabelled icon buttons. VoiceOver reads "button" with no meaning; use
Button("Cart", systemImage: "cart")or.accessibilityLabel. - Profiling debug builds. Debug builds are much slower; profile a release build on a real device.
- Missing privacy information. Apps and SDKs that use required-reason APIs need a privacy manifest, or uploads are rejected.
- Releasing to everyone at once. Use phased release and TestFlight to catch crashes on a small group first.
Key points
- Design for VoiceOver, Dynamic Type, contrast and touch targets from the start; audit with Accessibility Inspector.
- Profile release builds with Instruments and fix the measured bottleneck.
- Ship through TestFlight, complete privacy details and manifests, pass App Review.
- Roll out gradually with phased release and watch crash reports.
Further reading: Accessibility in SwiftUI, App Review Guidelines, TestFlight.
Practice questions
How do you make a custom SwiftUI row work well with VoiceOver?
Answer
Combine its parts into one element (.accessibilityElement(children: .combine) or .ignore plus a label), give it a concise label with the important information, hide decorative images, add traits such as .isButton, and provide actions for swipe gestures.
What steps does an iOS app go through from build to users?
Answer
Archive and upload to App Store Connect, test with TestFlight (internal, then external testers), complete the listing, privacy details and privacy manifest, submit for App Review, then release, ideally as a phased release while monitoring crashes and feedback.
Open in the app to mark lessons complete, take notes and track your progress.