Skip to content

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

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