DOMAIN:IOS_DEVELOPMENT:PLATFORM_CONSTRAINTS¶
OWNER: martijn
ALSO_USED_BY: valentin, urszula/maxim (API design for mobile)
UPDATED: 2026-03-24
SOURCE: Apple Developer Documentation, iOS SDK Reference
CONSTRAINTS:MEMORY¶
RULE: iOS does NOT have swap space — when memory runs out, the app is killed (no warning)
RULE: iOS gives apps limited memory based on total device RAM
RULE: typical budget: 200-400 MB for foreground app on modern devices
RULE: jetsam (iOS memory pressure system) kills apps with highest memory footprint first
RULE: didReceiveMemoryWarning() or UIApplication.didReceiveMemoryWarningNotification may fire — release caches
CHECK: profile memory usage with Instruments → Allocations
CHECK: no retain cycles (use [weak self] in closures, weak references in delegates)
CHECK: images loaded at display size, not full resolution
CHECK: large collections are lazy-loaded
ANTI_PATTERN: loading all images from API at full resolution into memory
FIX: use thumbnail URLs for lists, full resolution only for detail view. Use UIImage.prepareThumbnail(of:).
ANTI_PATTERN: strong reference cycles in closures
FIX: [weak self] in escaping closures, especially in async callbacks and Combine subscribers
TOOL: Instruments → Leaks (detect memory leaks)
TOOL: Instruments → Allocations (track memory growth)
TOOL: Xcode → Memory Graph Debugger (visualize retain cycles)
CONSTRAINTS:BACKGROUND_EXECUTION¶
RULE: iOS aggressively suspends/terminates background apps
RULE: there is NO "run in background indefinitely" permission (except audio, location, VoIP, Bluetooth)
Allowed Background Modes¶
| Mode | Info.plist Key | Duration | Use Case |
|---|---|---|---|
| Background fetch | fetch |
~30 seconds | Periodic data refresh |
| Remote notifications | remote-notification |
~30 seconds | Silent push → data sync |
| Background processing | BGProcessingTask | Minutes (device-dependent) | ML training, maintenance |
| Audio | audio |
Indefinite (while playing) | Music, podcast apps |
| Location | location |
Indefinite (with permission) | Navigation, tracking |
| VoIP | voip |
Via PushKit | Voice/video calls |
| Bluetooth | bluetooth-central/peripheral |
Indefinite | BLE accessories |
| Background URL session | URLSession background config | Until complete | Large downloads/uploads |
BGTaskScheduler (Modern Background Tasks)¶
// Register in AppDelegate.didFinishLaunching
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.client.app.refresh",
using: nil
) { task in
handleAppRefresh(task: task as! BGAppRefreshTask)
}
// Schedule
let request = BGAppRefreshTaskRequest(identifier: "com.client.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try BGTaskScheduler.shared.submit(request)
RULE: BGAppRefreshTask = ~30 seconds, system decides when
RULE: BGProcessingTask = longer, runs when device is charging + connected to Wi-Fi
RULE: iOS learns usage patterns — frequently-used apps get more background time
RULE: NEVER rely on exact timing — background tasks are opportunistic
ANTI_PATTERN: expecting background task to run at exact scheduled time
FIX: design for eventual consistency — task runs "sometime after" earliest begin date
ANTI_PATTERN: doing network calls in applicationDidEnterBackground without background task
FIX: request beginBackgroundTask(withName:) for brief work, or use BGTaskScheduler
CONSTRAINTS:APP_TRANSPORT_SECURITY¶
STANDARD: Apple — App Transport Security (ATS)
RULE: ALL network connections MUST use HTTPS (TLS 1.2+) by default
RULE: HTTP connections are blocked unless explicitly exempted in Info.plist
RULE: App Store Review will question ATS exceptions
Exceptions (Info.plist)¶
<key>NSAppTransportSecurity</key>
<dict>
<!-- NEVER use this in production: -->
<!-- <key>NSAllowsArbitraryLoads</key><true/> -->
<!-- Per-domain exception (if client API is HTTP-only): -->
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.client.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
RULE: NSAllowsArbitraryLoads = true is NEVER acceptable in production
RULE: per-domain exceptions require justification in App Store review notes
RULE: IF client's backend API is HTTP-only THEN fix the backend, do not exempt in app
ANTI_PATTERN: disabling ATS globally during development and forgetting to re-enable
FIX: never disable globally — add per-domain exceptions only if needed, remove before submission
CONSTRAINTS:PUSH_NOTIFICATIONS¶
APNs (Apple Push Notification service) Setup¶
PREREQUISITES:
1. App ID with Push Notifications capability enabled
2. APNs authentication key (.p8) OR APNs certificate (.p12)
3. Backend configured to send pushes via APNs
Authentication Key (Recommended)¶
RULE: use APNs authentication key (.p8) over certificates
REASON: key never expires, works for all apps in team, simpler to manage
CREATED: Apple Developer Portal → Keys → Create Key → Apple Push Notifications Service
NOTE: download once — Apple does not store it. Save securely.
| Property | Key (.p8) | Certificate (.p12) |
|---|---|---|
| Expiry | Never | 1 year |
| Scope | All apps in team | Per app |
| Setup | Simpler | Per-app + per-environment |
| Format | JWT-based auth | TLS client cert |
Permission Request¶
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound]
) { granted, error in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
RULE: request permission at a contextually appropriate moment — NOT on first launch
RULE: explain WHY before showing system prompt (custom pre-permission screen)
RULE: handle denial gracefully — app must work without notifications
RULE: check authorization status before sending from backend
Device Token¶
RULE: device tokens can change — always send latest token to backend on app launch
RULE: tokens are different for sandbox vs production APNs environments
RULE: backend must handle token registration and deregistration
ANTI_PATTERN: requesting push permission immediately on first launch
FIX: wait for context (e.g., after user completes onboarding, after first meaningful action)
CONSTRAINTS:APP_SIZE¶
| Limit | Value | Applies To |
|---|---|---|
| App Store download limit (cellular) | 200 MB | Over-the-air download without Wi-Fi |
| App Store max app bundle | 4 GB | Total IPA size |
| On-Demand Resources max | 20 GB | Additional assets downloaded at runtime |
| App Thinning | Automatic | Apple delivers only device-specific assets |
RULE: keep initial download under 200 MB to avoid Wi-Fi-required warning
RULE: use Asset Catalogs — App Thinning removes unused device assets (@1x/@2x/@3x)
RULE: use On-Demand Resources for large content (game levels, video tutorials)
RULE: compile with bitcode (Xcode default) for Apple optimization
CHECK: Archive → Estimate Size in Xcode Organizer
CHECK: App Store Connect → build details → file sizes per device
ANTI_PATTERN: bundling all image resolutions manually
FIX: use Asset Catalogs — provide @1x, @2x, @3x; App Thinning delivers correct one
ANTI_PATTERN: bundling large video/audio assets in the app bundle
FIX: stream from CDN or use On-Demand Resources
CONSTRAINTS:DATA_PERSISTENCE¶
Options¶
| Technology | Use Case | Structured | Sync |
|---|---|---|---|
| UserDefaults | Small key-value (settings, flags) | No | No (iCloud KV possible) |
| Keychain | Secrets (tokens, passwords) | No | iCloud Keychain |
| SwiftData | Structured app data (iOS 17+) | Yes | CloudKit optional |
| Core Data | Structured app data (legacy) | Yes | CloudKit optional |
| File system (Documents/) | Documents, exports | No | iCloud Drive |
| File system (Caches/) | Temporary/recreatable data | No | No, system may purge |
| File system (tmp/) | Truly temporary | No | No, purged aggressively |
| SQLite (direct) | High-performance queries | Yes | Manual |
RULE: use SwiftData for new projects (iOS 17+)
RULE: use Core Data only if supporting iOS 16 or lower
RULE: NEVER store secrets in UserDefaults — use Keychain
RULE: NEVER store user data in Caches/ or tmp/ — system will delete it
RULE: files in Documents/ are backed up by iCloud — exclude large recreatable files
Offline-First Pattern¶
RULE: GE apps must work offline for core functionality where feasible
RULE: pattern: local-first → sync when online → conflict resolution
1. Write to local store (SwiftData/Core Data) immediately
2. Queue sync operations (background URL session)
3. When network available → push changes to API
4. Pull remote changes → merge with local
5. Handle conflicts (last-write-wins, or manual resolution)
ANTI_PATTERN: showing empty state when offline
FIX: cache last-known data locally, show with "offline" indicator
ANTI_PATTERN: using UserDefaults for anything other than small settings
FIX: UserDefaults is plist-backed, not designed for large data. Use SwiftData/Core Data.
CONSTRAINTS:NETWORKING¶
URLSession Best Practices¶
RULE: use async/await (Swift concurrency) for network calls
RULE: use Codable for JSON parsing
RULE: handle ALL HTTP status codes, not just 200
RULE: implement retry with exponential backoff for transient failures
RULE: respect Retry-After header
RULE: use URLSession configuration for timeouts
// Standard pattern
func fetchItems() async throws -> [Item] {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw APIError.serverError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? -1)
}
return try JSONDecoder().decode([Item].self, from: data)
}
Background Downloads/Uploads¶
RULE: for large file transfers, use background URLSession configuration
RULE: background sessions continue even if app is suspended/terminated
RULE: implement urlSession(_:downloadTask:didFinishDownloadingTo:) delegate
Network Reachability¶
RULE: use NWPathMonitor (Network framework) — NOT Reachability.swift
RULE: do NOT pre-check connectivity before making request — just make the request and handle failure
RULE: show offline UI based on path monitor status
ANTI_PATTERN: checking reachability before every network call
FIX: just make the call — handle the error. Reachability can give false positives.
CONSTRAINTS:CONCURRENCY¶
Swift Concurrency (async/await)¶
RULE: use async/await for all asynchronous work (iOS 15+)
RULE: use @MainActor for UI updates
RULE: use Task {} to bridge from synchronous to async context
RULE: use TaskGroup for parallel async operations
RULE: use actors for shared mutable state
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
func loadItems() async {
do {
items = try await apiService.fetchItems()
} catch {
// handle error on main actor
}
}
}
RULE: strict concurrency checking enabled (SWIFT_STRICT_CONCURRENCY = complete)
RULE: in Swift 6, data race safety is enforced at compile time — adopt early
ANTI_PATTERN: using DispatchQueue.main.async for UI updates in async context
FIX: mark function or class with @MainActor
ANTI_PATTERN: using completion handlers when async/await is available
FIX: wrap legacy callback APIs with withCheckedContinuation or withCheckedThrowingContinuation