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.

Create a free account to save this page, take notes and track your progress. Sign in

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:

  1. Archive in Xcode and upload to App Store Connect (or use Xcode Cloud).
  2. Test with TestFlight: internal testers immediately, external testers after a short beta review.
  3. Fill in the listing, privacy details ("nutrition labels") and a privacy manifest for your app and SDKs.
  4. Submit for App Review; follow the App Review Guidelines (sign-in with a demo account, no placeholder content).
  5. 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:

An accessibility label built from the data
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.

Was this page helpful?