iOS Development with Swift
A comprehensive technical guide covering Swift language features, UIKit and SwiftUI, dependency management, Fastlane CI/CD, App Store Connect automation, push notifications, biometric authentication, Core Data, SwiftData, Combine, App Clips, WidgetKit, testing, Instruments profiling, and accessibility.
Table of Contents
- Swift Language Features
- UIKit and SwiftUI
- CocoaPods and SPM Dependency Management
- Fastlane (match, gym, deliver, pilot)
- App Store Connect Automation
- Push Notifications (APNs)
- Biometric Authentication
- Core Data and SwiftData
- Combine Framework
- App Clips
- WidgetKit
- Testing (XCTest and XCUITest)
- Instruments Profiling
- Accessibility
- Latest Features (2025-2026)
1. Swift Language Features
Protocols and Protocol-Oriented Programming
Swift favors protocol-oriented programming over class inheritance. Protocols with default implementations, associated types, and protocol extensions provide flexible composition.
protocol Cacheable {
associatedtype Key: Hashable
var cacheKey: Key { get }
var ttl: TimeInterval { get }
}
extension Cacheable {
var ttl: TimeInterval { 300 } // default 5 min
}
protocol NetworkService {
func fetch<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}
// Combine protocols for composition
typealias AuthenticatedNetworkService = NetworkService & AuthProvider
struct APIClient: AuthenticatedNetworkService {
func fetch<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
var request = endpoint.urlRequest
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, 200...299 ~= http.statusCode else {
throw APIError.invalidResponse
}
return try JSONDecoder().decode(T.self, from: data)
}
}
Swift Concurrency (async/await, Actors)
Swift's structured concurrency model with async/await, TaskGroup, and actor provides compile-time data race safety. The @MainActor annotation ensures UI updates happen on the main thread.
// Actor for thread-safe state
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private var inFlight: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
if let cached = cache[url] { return cached }
if let existing = inFlight[url] { return try await existing.value }
let task = Task {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.decodeFailed
}
return image
}
inFlight[url] = task
let image = try await task.value
cache[url] = image
inFlight[url] = nil
return image
}
}
// @MainActor for UI-bound code
@MainActor
class ProfileViewController: UIViewController {
func loadProfile() {
Task {
let profile = try await apiClient.fetch(.profile)
nameLabel.text = profile.name // guaranteed main thread
}
}
}
Generics and Result Type
Generics with type constraints enable reusable, type-safe abstractions. The Result type provides explicit error handling as an alternative to try/catch.
// Generic network response wrapper
struct PaginatedResponse<T: Decodable>: Decodable {
let items: [T]
let total: Int
let page: Int
let hasMore: Bool
}
// Generic repository pattern
protocol Repository {
associatedtype Entity: Identifiable & Decodable
func getAll() async throws -> [Entity]
func getById(_ id: Entity.ID) async throws -> Entity
func save(_ entity: Entity) async throws -> Entity
func delete(_ id: Entity.ID) async throws
}
// Result-based error handling
enum AppError: Error, LocalizedError {
case network(URLError)
case decoding(DecodingError)
case unauthorized
case serverError(statusCode: Int, message: String)
var errorDescription: String? {
switch self {
case .network(let error): return "Network error: \(error.localizedDescription)"
case .decoding: return "Failed to parse server response"
case .unauthorized: return "Session expired. Please log in again."
case .serverError(_, let msg): return msg
}
}
}
Property Wrappers
Property wrappers encapsulate reusable property logic. SwiftUI relies on them extensively (@State, @Binding, @StateObject, @EnvironmentObject), and custom wrappers are powerful for validation, persistence, and access control.
// Custom property wrapper for UserDefaults persistence
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
let container: UserDefaults
init(key: String, defaultValue: T, container: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.container = container
}
var wrappedValue: T {
get { container.object(forKey: key) as? T ?? defaultValue }
set { container.set(newValue, forKey: key) }
}
}
// Clamped value property wrapper
@propertyWrapper
struct Clamped<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
// Usage in a settings model
class AppSettings: ObservableObject {
@UserDefault(key: "onboardingComplete", defaultValue: false)
var onboardingComplete: Bool
@UserDefault(key: "preferredUnit", defaultValue: "kg")
var preferredUnit: String
}
// Usage in SwiftUI with built-in wrappers
struct SettingsView: View {
@AppStorage("notificationsEnabled") private var notifications = true
@Environment(\.dismiss) private var dismiss
var body: some View {
Toggle("Enable Notifications", isOn: $notifications)
}
}
2. UIKit and SwiftUI
SwiftUI Declarative Views
SwiftUI uses a declarative syntax where you describe the view hierarchy and SwiftUI handles rendering, diffing, and animation. State management is built-in with property wrappers.
struct WorkoutListView: View {
@StateObject private var viewModel = WorkoutViewModel()
@State private var showingAddSheet = false
var body: some View {
NavigationStack {
List {
ForEach(viewModel.workouts) { workout in
NavigationLink(value: workout) {
WorkoutRow(workout: workout)
}
}
.onDelete { indexSet in
viewModel.delete(at: indexSet)
}
}
.navigationTitle("Workouts")
.navigationDestination(for: Workout.self) { workout in
WorkoutDetailView(workout: workout)
}
.toolbar {
Button("Add") { showingAddSheet = true }
}
.sheet(isPresented: $showingAddSheet) {
AddWorkoutView(viewModel: viewModel)
}
.refreshable { await viewModel.refresh() }
}
}
}
UIKit Programmatic Layout
For complex layouts and legacy codebases, UIKit with programmatic Auto Layout remains essential. SnapKit or manual constraints via NSLayoutConstraint provide full control.
class WorkoutCell: UITableViewCell {
private let titleLabel = UILabel()
private let durationLabel = UILabel()
private let iconView = UIImageView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") }
private func setupViews() {
[iconView, titleLabel, durationLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview($0)
}
NSLayoutConstraint.activate([
iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
iconView.widthAnchor.constraint(equalToConstant: 40),
iconView.heightAnchor.constraint(equalToConstant: 40),
titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12),
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
durationLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
durationLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
durationLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
])
}
}
3. CocoaPods and SPM Dependency Management
CocoaPods
CocoaPods is Ruby-based and uses a Podfile for dependency declaration. It modifies the Xcode workspace, adding a Pods project alongside your app project.
# Podfile
platform :ios, '15.0'
use_frameworks!
inhibit_all_warnings!
target 'MyApp' do
# Networking
pod 'Alamofire', '~> 5.9'
pod 'Kingfisher', '~> 7.0'
# Analytics
pod 'Firebase/Analytics'
pod 'Firebase/Crashlytics'
# UI
pod 'SnapKit', '~> 5.7'
pod 'lottie-ios', '~> 4.4'
target 'MyAppTests' do
inherit! :search_paths
pod 'Quick', '~> 7.0'
pod 'Nimble', '~> 13.0'
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end
Swift Package Manager (SPM)
SPM is Apple's native dependency manager, integrated directly into Xcode. It uses Package.swift manifest files and supports binary dependencies, plugins, and conditional compilation.
// Package.swift
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "AppCore",
platforms: [.iOS(.v15), .macOS(.v13)],
products: [
.library(name: "AppCore", targets: ["AppCore"]),
.library(name: "AppNetworking", targets: ["AppNetworking"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.1.0"),
],
targets: [
.target(
name: "AppCore",
dependencies: [.product(name: "Algorithms", package: "swift-algorithms")]
),
.target(
name: "AppNetworking",
dependencies: ["AppCore", .product(name: "Dependencies", package: "swift-dependencies")]
),
.testTarget(name: "AppCoreTests", dependencies: ["AppCore"]),
]
)
4. Fastlane (match, gym, deliver, pilot)
match: Code Signing Management
match stores code signing certificates and provisioning profiles in a shared Git repo or cloud storage. It ensures all team members and CI machines use the same credentials.
# Matchfile
git_url("git@gitlab.com:your-org/ios-certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.example.app", "com.example.app.widget"])
username("ios@example.com")
team_id("ABC123DEF")
# Fastfile - sync certificates
lane :certificates do
match(type: "development", readonly: true)
match(type: "appstore", readonly: true)
end
gym: Build Automation
gym (alias for build_app) handles xcodebuild invocations for archiving and exporting IPAs. It manages workspace detection, scheme selection, and export options.
# Gymfile
workspace("MyApp.xcworkspace")
scheme("MyApp")
clean(true)
output_directory("./build")
export_method("app-store")
include_bitcode(false)
export_options({
compileBitcode: false,
uploadSymbols: true
})
# Fastfile - complete build lane
lane :build do
increment_build_number(
build_number: ENV["CI_PIPELINE_IID"] || latest_testflight_build_number + 1
)
certificates
gym
end
deliver: App Store Upload
deliver uploads IPA files, screenshots, metadata, and release notes to App Store Connect. It can auto-submit for review and manage phased releases.
# Deliverfile
app_identifier("com.example.app")
username("ios@example.com")
team_id("ABC123DEF")
submit_for_review(true)
automatic_release(false)
phased_release(true)
force(true)
reject_if_possible(true)
submission_information({
add_id_info_uses_idfa: false
})
# Fastfile - full deploy
lane :deploy do
build
deliver(
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
skip_screenshots: true,
precheck_include_in_app_purchases: false
)
slack(message: "iOS build submitted to App Store Review")
end
pilot: TestFlight Distribution
pilot (alias for upload_to_testflight) manages TestFlight builds, beta testers, and external testing groups. It uploads IPAs and distributes them to internal and external testers.
# Fastfile - TestFlight beta distribution
lane :beta do
build
pilot(
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
skip_waiting_for_build_processing: false,
distribute_external: true,
groups: ["QA Team", "Beta Testers"],
changelog: "Bug fixes and performance improvements",
beta_app_review_info: {
contact_email: "ios@example.com",
contact_first_name: "Jose",
contact_last_name: "Nobile",
contact_phone: "+57 300 000 0000",
demo_account_name: "demo@example.com",
demo_account_password: "your-secure-password"
}
)
slack(message: "New TestFlight build available for testers")
end
# Manage beta testers
lane :add_testers do
pilot(
distribute_only: true,
groups: ["External Beta"],
notify_external_testers: true
)
end
5. App Store Connect Automation
App Store Connect API
The App Store Connect API provides programmatic access to manage apps, TestFlight builds, beta testers, and app metadata. Authentication uses JWT tokens signed with a .p8 private key, and Fastlane wraps this API for common workflows.
# Generate API key (stored in CI secrets)
# Key ID, Issuer ID, and .p8 private key file
# Fastlane API key configuration
lane :setup_api do
app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_filepath: ENV["ASC_KEY_PATH"],
duration: 1200,
in_house: false
)
end
# Direct API usage with Swift for custom tooling
# POST https://api.appstoreconnect.apple.com/v1/betaTesters
# Authorization: Bearer <jwt-token>
# Content-Type: application/json
Metadata and Localization
Fastlane manages App Store metadata as local files: descriptions, keywords, release notes, and screenshots organized by locale. This integrates naturally with version control.
# Directory structure
# fastlane/metadata/en-US/
# description.txt
# keywords.txt
# release_notes.txt
# name.txt
# subtitle.txt
# fastlane/metadata/es-MX/
# description.txt
# ...
# fastlane/screenshots/en-US/
# 1_home.png
# 2_workout.png
# Automated screenshot capture
lane :screenshots do
capture_screenshots(
workspace: "MyApp.xcworkspace",
scheme: "MyAppUITests",
devices: ["iPhone 15 Pro Max", "iPhone SE (3rd generation)", "iPad Pro (12.9-inch)"],
languages: ["en-US", "es-MX"],
clear_previous_screenshots: true
)
frame_screenshots(silver: true)
end
6. Push Notifications (APNs)
APNs Configuration and Registration
Apple Push Notification service (APNs) requires an authentication key or certificate, entitlements configuration, and device token registration. Token-based authentication (p8 key) is preferred over certificate-based.
// AppDelegate - Push registration
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound, .provisional]
) { granted, error in
guard granted else { return }
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
Task { await APIClient.shared.registerPushToken(token) }
}
// Handle notification when app is in foreground
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async
-> UNNotificationPresentationOptions {
return [.banner, .badge, .sound]
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let userInfo = response.notification.request.content.userInfo
DeepLinkRouter.handle(userInfo: userInfo)
}
}
Rich Notifications and Notification Service Extension
Notification Service Extensions allow modifying notification content before display, enabling image attachments, encrypted payload decryption, and analytics tracking.
// NotificationService.swift (Notification Service Extension target)
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
guard let content = bestAttemptContent,
let imageURL = content.userInfo["image_url"] as? String,
let url = URL(string: imageURL) else {
contentHandler(request.content)
return
}
downloadImage(from: url) { attachment in
if let attachment = attachment {
content.attachments = [attachment]
}
contentHandler(content)
}
}
}
7. Biometric Authentication
Face ID and Touch ID with LocalAuthentication
The LocalAuthentication framework provides Face ID and Touch ID with a unified API. The system handles the biometric prompt UI. Combine it with Keychain for secure credential storage.
import LocalAuthentication
class BiometricAuthManager {
enum BiometricType { case none, touchID, faceID }
var availableBiometric: BiometricType {
let context = LAContext()
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else {
return .none
}
switch context.biometryType {
case .touchID: return .touchID
case .faceID: return .faceID
default: return .none
}
}
func authenticate() async throws -> Bool {
let context = LAContext()
context.localizedCancelTitle = "Use Password"
context.localizedFallbackTitle = "Enter PIN"
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
throw AuthError.biometricUnavailable(error?.localizedDescription ?? "Unknown")
}
return try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to access your workouts"
)
}
}
// Keychain integration for secure token storage
class SecureTokenStore {
func save(token: String, for account: String) throws {
let data = Data(token.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrAccessControl as String: try createAccessControl()
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }
}
private func createAccessControl() throws -> SecAccessControl {
var error: Unmanaged<CFError>?
guard let access = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet,
&error
) else {
throw KeychainError.accessControlFailed
}
return access
}
}
8. Core Data and SwiftData
Core Data Stack and NSManagedObject
Core Data provides object-graph persistence with an SQLite backing store. The NSPersistentContainer simplifies stack setup, and NSManagedObject subclasses define the data model in code.
// Core Data stack setup
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "AppModel")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Core Data load failed: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func save() {
let context = container.viewContext
guard context.hasChanges else { return }
do {
try context.save()
} catch {
let nsError = error as NSError
print("Core Data save error: \(nsError)")
}
}
}
// Background context for heavy writes
func importWorkouts(_ data: [WorkoutDTO]) async {
let context = PersistenceController.shared.container.newBackgroundContext()
await context.perform {
for dto in data {
let workout = WorkoutEntity(context: context)
workout.id = dto.id
workout.name = dto.name
workout.duration = dto.duration
}
try? context.save()
}
}
SwiftData
SwiftData is Apple's modern persistence framework that replaces Core Data boilerplate with Swift macros. The @Model macro auto-generates the schema, and @Query provides live-updating fetches in SwiftUI.
import SwiftData
@Model
class Workout {
var name: String
var duration: TimeInterval
var calories: Int
var completedAt: Date
@Relationship(deleteRule: .cascade) var exercises: [Exercise]
init(name: String, duration: TimeInterval, calories: Int) {
self.name = name
self.duration = duration
self.calories = calories
self.completedAt = .now
self.exercises = []
}
}
@Model
class Exercise {
var name: String
var sets: Int
var reps: Int
var workout: Workout?
init(name: String, sets: Int, reps: Int) {
self.name = name
self.sets = sets
self.reps = reps
}
}
// SwiftUI integration with @Query
struct WorkoutHistoryView: View {
@Query(sort: \Workout.completedAt, order: .reverse)
private var workouts: [Workout]
@Environment(\.modelContext) private var context
var body: some View {
List(workouts) { workout in
VStack(alignment: .leading) {
Text(workout.name).font(.headline)
Text("\(Int(workout.duration / 60)) min")
.foregroundStyle(.secondary)
}
}
}
}
9. Combine Framework
Publishers, Operators, and Subscribers
Combine is Apple's reactive framework for processing asynchronous events. Publishers emit values over time, operators transform them, and subscribers consume the results. It integrates tightly with SwiftUI via @Published and ObservableObject.
import Combine
class WorkoutViewModel: ObservableObject {
@Published var searchText = ""
@Published var workouts: [Workout] = []
@Published var isLoading = false
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
init() {
// Debounced search with Combine pipeline
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.sink { [weak self] query in
self?.search(query)
}
.store(in: &cancellables)
}
func search(_ query: String) {
isLoading = true
APIClient.shared.searchWorkouts(query: query)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] results in
self?.workouts = results
}
)
.store(in: &cancellables)
}
}
// Combine with NotificationCenter
NotificationCenter.default
.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { _ in refreshData() }
.store(in: &cancellables)
// Combine with Timer
Timer.publish(every: 30, on: .main, in: .common)
.autoconnect()
.sink { _ in pollForUpdates() }
.store(in: &cancellables)
10. App Clips
App Clip Configuration and Invocation
App Clips are lightweight versions of your app (under 15 MB) that launch instantly from NFC tags, QR codes, App Clip Codes, or Safari banners. They share code with the main app target but have their own Info.plist and entitlements.
// App Clip entry point
@main
struct MyAppClip: App {
var body: some Scene {
WindowGroup {
ContentView()
.onContinueUserActivity(
NSUserActivityTypeBrowsingWeb
) { activity in
guard let url = activity.webpageURL else { return }
handleInvocation(url: url)
}
}
}
func handleInvocation(url: URL) {
// Parse URL to determine which experience to show
// e.g., https://example.com/clip/class/yoga-morning
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let classID = components?.queryItems?.first(where: { $0.name == "id" })?.value {
// Show class booking view for this specific class
NavigationState.shared.showClassBooking(classID: classID)
}
}
}
// Shared code between main app and App Clip
// Use the active compilation condition to differentiate
#if APPCLIP
// App Clip specific: prompt full app download
func promptFullAppInstall() {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
let config = SKOverlay.AppClipConfiguration(position: .bottom)
let overlay = SKOverlay(configuration: config)
overlay.present(in: scene)
}
#endif
11. WidgetKit
Widget Timeline Provider and SwiftUI Views
WidgetKit uses a TimelineProvider to supply snapshots and timeline entries that the system renders as SwiftUI views. Widgets support multiple families (small, medium, large) and use AppIntents for interactivity.
import WidgetKit
import SwiftUI
struct WorkoutEntry: TimelineEntry {
let date: Date
let workoutName: String
let calories: Int
let streakDays: Int
}
struct WorkoutWidgetProvider: TimelineProvider {
func placeholder(in context: Context) -> WorkoutEntry {
WorkoutEntry(date: .now, workoutName: "Morning Run", calories: 350, streakDays: 7)
}
func getSnapshot(in context: Context, completion: @escaping (WorkoutEntry) -> Void) {
let entry = WorkoutEntry(date: .now, workoutName: "HIIT Session", calories: 420, streakDays: 12)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WorkoutEntry>) -> Void) {
// Fetch latest workout from shared App Group container
let store = UserDefaults(suiteName: "group.com.example.app")
let name = store?.string(forKey: "lastWorkout") ?? "No workout"
let cals = store?.integer(forKey: "lastCalories") ?? 0
let streak = store?.integer(forKey: "streak") ?? 0
let entry = WorkoutEntry(date: .now, workoutName: name, calories: cals, streakDays: streak)
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: .now)!
completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
}
}
struct WorkoutWidget: Widget {
let kind = "WorkoutWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: WorkoutWidgetProvider()) { entry in
WorkoutWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Workout Tracker")
.description("See your latest workout and streak.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
12. Testing (XCTest and XCUITest)
Unit Testing with XCTest
XCTest is Apple's built-in testing framework. Unit tests validate logic in isolation using XCTestCase, assertions, and expectations for asynchronous code. Protocol-based dependency injection makes classes testable.
import XCTest
@testable import MyApp
final class WorkoutViewModelTests: XCTestCase {
var sut: WorkoutViewModel!
var mockAPI: MockAPIClient!
override func setUp() {
super.setUp()
mockAPI = MockAPIClient()
sut = WorkoutViewModel(apiClient: mockAPI)
}
override func tearDown() {
sut = nil
mockAPI = nil
super.tearDown()
}
func testFetchWorkoutsSuccess() async throws {
mockAPI.stubbedWorkouts = [
Workout(name: "HIIT", duration: 1800, calories: 400)
]
await sut.fetchWorkouts()
XCTAssertEqual(sut.workouts.count, 1)
XCTAssertEqual(sut.workouts.first?.name, "HIIT")
XCTAssertFalse(sut.isLoading)
XCTAssertNil(sut.errorMessage)
}
func testFetchWorkoutsNetworkError() async {
mockAPI.shouldFail = true
await sut.fetchWorkouts()
XCTAssertTrue(sut.workouts.isEmpty)
XCTAssertNotNil(sut.errorMessage)
}
func testCalorieCalculation() {
let workout = Workout(name: "Run", duration: 3600, calories: 0)
workout.exercises = [
Exercise(name: "Squats", sets: 3, reps: 12),
Exercise(name: "Lunges", sets: 3, reps: 10)
]
let estimated = sut.estimateCalories(for: workout)
XCTAssertGreaterThan(estimated, 0)
}
}
UI Testing with XCUITest
XCUITest drives the app through its UI for end-to-end testing. It launches the app in a separate process, queries accessibility elements, and performs taps, swipes, and text entry.
import XCTest
final class WorkoutFlowUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launchArguments = ["--uitesting"]
app.launchEnvironment = ["MOCK_API": "true"]
app.launch()
}
func testAddWorkoutFlow() throws {
// Navigate to workout list
app.tabBars.buttons["Workouts"].tap()
// Tap add button
app.navigationBars.buttons["Add"].tap()
// Fill in workout details
let nameField = app.textFields["Workout Name"]
XCTAssertTrue(nameField.waitForExistence(timeout: 3))
nameField.tap()
nameField.typeText("Morning HIIT")
// Select duration
app.buttons["30 min"].tap()
// Save
app.buttons["Save Workout"].tap()
// Verify workout appears in list
let cell = app.cells.staticTexts["Morning HIIT"]
XCTAssertTrue(cell.waitForExistence(timeout: 5))
}
func testDeleteWorkout() throws {
app.tabBars.buttons["Workouts"].tap()
let cell = app.cells.firstMatch
cell.swipeLeft()
app.buttons["Delete"].tap()
app.alerts.buttons["Confirm"].tap()
}
}
13. Instruments Profiling
Performance Profiling with Instruments
Instruments is Xcode's profiling suite for diagnosing performance issues. Key instruments include Time Profiler for CPU bottlenecks, Allocations for memory leaks, Core Animation for rendering, and Network for request analysis.
// Signpost API for custom performance measurement
import os.signpost
let log = OSLog(subsystem: "com.example.app", category: "Performance")
func loadWorkoutFeed() async {
let signpostID = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "LoadFeed", signpostID: signpostID)
let workouts = try? await apiClient.fetchWorkouts()
os_signpost(.end, log: log, name: "LoadFeed", signpostID: signpostID)
}
// MetricKit for production performance data
import MetricKit
class PerformanceManager: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
// Analyze launch time, hang rate, disk writes
if let launchMetric = payload.applicationLaunchMetrics {
let avgLaunch = launchMetric.histogrammedTimeToFirstDraw
.bucketEnumerator
analytics.track("launch_time", properties: ["data": avgLaunch])
}
}
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
// Crash diagnostics, hang diagnostics, disk write exceptions
if let crashes = payload.crashDiagnostics {
for crash in crashes {
analytics.track("crash", properties: [
"signal": crash.signal,
"termination": crash.terminationReason ?? "unknown"
])
}
}
}
}
}
// Common Instruments workflow checklist:
// 1. Time Profiler: identify hot paths and main thread blocking
// 2. Allocations: detect memory leaks and excessive allocations
// 3. Leaks: find retain cycles (especially in closures and delegates)
// 4. Core Animation: diagnose offscreen rendering and blending
// 5. Network: analyze request latency and payload sizes
14. Accessibility
VoiceOver, Dynamic Type, and Accessibility API
iOS accessibility covers VoiceOver screen reading, Dynamic Type for scalable text, color contrast, reduced motion, and the accessibility API for custom elements. Both SwiftUI and UIKit provide modifiers and properties to make apps usable for everyone.
// SwiftUI accessibility
struct WorkoutRow: View {
let workout: Workout
var body: some View {
HStack {
Image(systemName: "flame.fill")
.accessibilityHidden(true) // decorative
VStack(alignment: .leading) {
Text(workout.name)
.font(.headline)
Text("\(workout.calories) cal")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(workout.name), \(workout.calories) calories")
.accessibilityHint("Double tap to view workout details")
.accessibilityAddTraits(.isButton)
}
}
// Dynamic Type support
struct StatsView: View {
@ScaledMetric(relativeTo: .title) private var iconSize = 24.0
var body: some View {
HStack {
Image(systemName: "heart.fill")
.frame(width: iconSize, height: iconSize)
Text("Heart Rate")
.dynamicTypeSize(...DynamicTypeSize.accessibility3)
}
}
}
// UIKit accessibility
class WorkoutDetailCell: UITableViewCell {
func configure(with workout: Workout) {
isAccessibilityElement = true
accessibilityLabel = "\(workout.name), \(workout.duration) minutes"
accessibilityTraits = .button
accessibilityHint = "Opens workout details"
// Support Dynamic Type
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = true
}
}
// Reduced motion support
struct AnimatedView: View {
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
Circle()
.animation(reduceMotion ? nil : .spring(), value: isExpanded)
}
}
// Accessibility audit in tests
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
try app.performAccessibilityAudit()
}
15. Latest Features (2025-2026)
Swift 6 Strict Concurrency
Swift 6 introduces compile-time data race elimination, a paradigm shift for concurrency safety. The compiler now enforces that mutable state is never shared across concurrent execution contexts without proper synchronization. Stricter @MainActor enforcement ensures UI code runs where it should. This eliminates an entire class of runtime bugs that were previously impossible to catch before deployment.
Swift 6.2: Approachable Concurrency
Swift 6.2 focuses on making concurrency more approachable. Main-actor isolation by default option simplifies the common case where code runs on the main thread. New InlineArray and Span types provide efficient fixed-size collections and memory-safe buffer access. WebAssembly support enables Swift to target the web. The new Subprocess package offers a concurrency-friendly API for external processes. A modern NotificationCenter API uses concrete types instead of strings. Improved async debugging in Xcode shows task hierarchies and suspension points.
Foundation Models Framework (iOS 26)
The Foundation Models framework provides access to the on-device LLM that powers Apple Intelligence, with a native Swift API. Available on iOS 26, iPadOS 26, macOS Tahoe, and visionOS, it enables natural language understanding, content generation, and summarization — all running entirely on-device for privacy. Key capabilities include Guided Generation (using @Generable and @Guide macros for structured output), tool calling for autonomous code execution, and streaming responses. Works on any Apple Intelligence-compatible device with as few as three lines of code.
SwiftUI Updates
SwiftUI introduces Liquid Glass, a new design material that brings depth and translucency to interfaces. 3D Swift Charts with RealityKit integration enable data visualizations in spatial computing. The new Animatable macro simplifies custom animations by auto-generating conformance. SF Symbols received new draw-on animations. Xcode 26.5 beta adds new StoreKit APIs for monthly subscriptions with 12-month commitment billing. These updates push SwiftUI further as the primary UI framework for all Apple platforms.
Swift 6.3 and WWDC 2026
Swift 6.3 has entered its release process with a branch cut targeting Spring 2026, expected to debut at WWDC 2026 (June 2026). Apple skipped iOS 19 through iOS 25 in a one-time version number jump, so iOS 26 follows iOS 18 directly. WWDC 2026 is expected to announce iOS 27, macOS 27, and further refinements to the Liquid Glass design language. Developers should prepare for Swift 6.3 by ensuring full Swift 6 strict concurrency compliance.
visionOS Development
visionOS development introduces immersive spaces for full spatial experiences, spatial layout for positioning content in 3D space, volumes for bounded 3D content, and 3D alignment APIs for precise object positioning. SwiftUI is the primary framework, with RealityKit handling 3D rendering. Apps can progressively enhance from 2D windows to full immersion.
visionOS 26: Enhanced Spatial Computing
visionOS 26 (announced at WWDC 2025) adds enhanced volumetric APIs that combine SwiftUI, RealityKit, and ARKit for more engaging spatial apps and games. RealityKit entities and their animations are now observable and usable directly in SwiftUI views. Gesture handlers written in SwiftUI can be attached directly to RealityKit entities. Faster hand tracking and support for spatial accessories expand input possibilities. SwiftUI spatial layout provides built-in support for animations, resizing, and state management in 3D — letting developers build spatial apps with the same 2D tools they already know.
Concurrency Migration Guide
Migrating from Swift 5 to Swift 6 concurrency requires systematic changes: enable strict concurrency checking incrementally per target, annotate types with Sendable conformance, mark actor-isolated properties and methods, replace DispatchQueue.main with @MainActor, and audit all closure captures. Start with the strictest warnings-as-errors on new code while gradually migrating existing modules.