iOS Code Review Checklist: 20 Critical Issues to Audit Before Production
Most iOS codebases look fine until they hit production. Then a memory leak crashes the app on first real-world load. A missing privacy manifest triggers an App Store rejection. A cloud-dependent AI feature stops working offline. This checklist covers 20 of the most common and most damaging issues to audit before you ship.
Most iOS codebases look fine until they hit production. Then a memory leak crashes the app on the first real-world load. A missing privacy manifest triggers an App Store rejection. A cloud-dependent AI feature stops working the moment a field team goes offline.
These are not edge cases. They are the standard failure modes in iOS codebases built under startup conditions.
Work through this checklist before you ship. If you find more than a handful of these in your codebase, you have a technical debt problem worth addressing now rather than post-launch.
Architecture and Code Structure
1. No Clear Separation of Concerns
The most common structural problem in startup iOS codebases is business logic mixed directly into views. Your views should not own network calls, data transformation, or state management beyond presentation.
Check for: network calls inside View structs or UIViewController subclasses, business rules embedded in onAppear or viewDidLoad, no clear ViewModel or repository layer.
What to look for:
// Red flag: network call directly in a View
struct DashboardView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { ... }
.onAppear {
URLSession.shared.dataTask(with: url) { ... }.resume() // wrong
}
}
}
The fix is not a specific architecture pattern. It is consistent boundaries — MVVM, MVI, or TCA applied uniformly.
2. Massive View Controllers or Bloated SwiftUI Views
A UIViewController over 500 lines is a warning sign. A SwiftUI View with more than 200 lines of body code is a different version of the same problem.
Check for: deeply nested SwiftUI view hierarchies, multiple @State variables managing unrelated concerns, view files that import business-layer frameworks directly.
3. Missing Dependency Injection
Singletons and static shared instances are convenient during early development. They become a maintenance and testability problem fast.
Check for: ServiceName.shared accessed directly inside views or view models, no protocol abstractions over services, test targets that cannot run without live network or database connections.
4. Hardcoded Configuration Values
API keys, base URLs, feature flags, and environment-specific configuration hardcoded directly in source files are a security and maintainability risk.
Check for: string literals that look like URLs or API keys in source files, no environment-based configuration system (Debug.xcconfig vs Release.xcconfig), API keys committed to version control.
Performance
5. Main Thread Blocking
Any synchronous operation that can take more than a few milliseconds — network requests, disk reads, large data transformations — on the main thread produces frame drops visible as UI jank.
Check for: URLSession calls without async/await, FileManager operations in view lifecycle methods, Core Data fetches on the main context without performBackgroundTask.
// Red flag
let items = try context.fetch(request) // synchronous fetch on main thread
6. Memory Leaks and Retain Cycles
Retain cycles in SwiftUI are subtler than in UIKit because the closure capture semantics differ.
Check for: strong capture of self in Task closures without [weak self], @StateObject and @ObservedObject mixed incorrectly (causing unexpected retains), NotificationCenter observers not removed in onDisappear.
7. Unoptimized Image Loading
Large images loaded synchronously without caching or downsampling produce memory pressure and slow scroll performance.
Check for: Image(uiImage: UIImage(contentsOfFile:)) in list cells, no AsyncImage for remote images, no explicit size constraints on loaded images.
8. Excessive View Redraws in SwiftUI
The @Observable macro in iOS 17+ tracks property access at the field level. Earlier ObservableObject with @Published marks the entire object as changed when any published property changes.
Check for: ObservableObject models with many unrelated @Published properties where changes to one trigger redraws of views that observe another, large computed properties in View.body that run on every redraw.
Security and Privacy
9. Sensitive Data in UserDefaults
UserDefaults is unencrypted and backed up to iCloud by default. Authentication tokens, personal data, and any value that qualifies as sensitive under GDPR or HIPAA must not live in UserDefaults.
Check for: UserDefaults.standard.set(token, forKey: "authToken"), user personal data stored in UserDefaults, UserDefaults keys containing health or financial data.
Fix: Use the iOS Keychain for secrets and authentication tokens. Use NSFileProtectionComplete for sensitive data files.
10. Missing or Weak Authentication
Check for: authentication tokens stored in UserDefaults rather than Keychain, biometric authentication bypassed on failure without user confirmation, no automatic session invalidation after inactivity period.
11. Unvalidated External Input
Any data arriving from a network response, a URL scheme, or a Share Extension must be validated before use.
Check for: direct use of URL query parameters without validation, JSON decoded directly into models without value-range validation on critical fields, no sanitization of user-supplied content displayed in web views.
12. Insecure Local File Storage
Check for: sensitive files written without NSFileProtectionComplete data protection, temporary files in tmp/ not cleaned up after use, files stored in the Documents/ directory that are not user-visible documents (these are backed up to iCloud by default).
// Correct: write sensitive file with protection
let url = documentsURL.appendingPathComponent("sensitive.dat")
try data.write(to: url, options: .completeFileProtection)
Data Layer and Persistence
13. No Offline-First Strategy
An app with no offline strategy is not an oversight — it is a commitment to a server-dependent architecture that will require structural rework when users encounter connectivity failure.
Check for: no local persistence layer, UI that shows error states rather than cached content during offline periods, writes that fail silently when the network is unavailable.
14. NSManagedObjectContext Threading Violations
Passing NSManagedObject instances across thread boundaries without using NSManagedObjectID produces crashes that are intermittent, hard to reproduce, and expensive to debug.
Check for: NSManagedObject instances stored in @State or passed in closures across actor boundaries, viewContext used for writes rather than background contexts, no explicit perform calls when accessing a private context from a different thread.
15. CloudKit Sync Without Conflict Resolution
Default NSPersistentCloudKitContainer setup without explicit merge policy silently drops conflicting writes.
Check for: NSPersistentCloudKitContainer initialized without viewContext.mergePolicy set, no subscriber to NSPersistentCloudKitContainer.eventChangedNotification, server-assigned integer IDs that will collide when two offline devices sync.
App Store Compliance
16. Missing or Incorrect Privacy Manifest
Since Spring 2024, Apple requires a PrivacyInfo.xcprivacy manifest for all apps and third-party SDKs. Missing or incomplete manifests trigger App Store rejection.
Check for: no PrivacyInfo.xcprivacy file in the app target, required reason APIs used without corresponding entries (UserDefaults, File timestamp, System boot time, Disk space, Active keyboard), third-party SDKs not declaring their own privacy manifests.
17. Vague NSUsageDescription Strings
App Store reviewers reject vague usage description strings. “To improve your experience” or “needed for app functionality” will not pass.
Check for: generic usage description strings in Info.plist, usage descriptions that do not name the specific data type accessed, missing descriptions for entitlements that are declared but used conditionally.
18. Incorrect Entitlements Configuration
Entitlements declared in the app that have no corresponding runtime use, or capabilities used at runtime that are not declared, both cause App Store rejection.
Check for: HealthKit entitlements declared but HealthKit not imported, Push Notification entitlements declared without APNS registration code, Background Modes declared without corresponding background work.
AI and On-Device ML Readiness
19. Cloud-Dependent AI With No Fallback
An AI feature that only works when a cloud API is reachable breaks in the scenarios where it matters most: field environments, travel, areas with poor coverage.
Check for: AI features that display an error state when offline with no cached or local fallback, inference calls made without checking network reachability first, no graceful degradation path documented in the feature specification.
Preferred pattern: on-device inference with Core ML or Apple Foundation Models as the primary path, cloud API as an optional enhancement for devices without the required Neural Engine tier.
20. Core ML Model Not Optimized for the Neural Engine
A Core ML model that runs on GPU or CPU when it could run on the Neural Engine produces 10–100x higher latency and significantly higher battery drain.
Check for: Core ML models not converted with ComputeUnits.all targeting, models larger than 50MB that should have been quantized, no inference profiling in Instruments to verify Neural Engine utilization.
// Correct: allow Core ML to target Neural Engine automatically
let config = MLModelConfiguration()
config.computeUnits = .all // includes Neural Engine
let model = try MyModel(configuration: config)
How to Use This Checklist
Run this checklist before any of these events:
- A new engineer joins the team and inherits the codebase
- Before App Store submission of a new major version
- Before adding a significant new feature (AI integration, Watch companion, HealthKit)
- After an App Store rejection — rejections often surface one issue; the checklist finds the adjacent ones
Finding 1–3 issues is normal. Finding 5+ is a signal that the codebase accumulated structural debt faster than it was addressed. At that point, a focused architectural remediation sprint is more cost-effective than addressing each issue individually while delivering features.