ios-swift
Expert iOS development skill covering SwiftUI, UIKit, Core Data, App Store guidelines, and performance optimization. Use this skill when building, reviewing, or debugging iOS apps - views, navigation, data persistence, animations, or submission preparation. Triggers on SwiftUI layout and state management, UIKit view controller lifecycle, Core Data model design and migrations, App Store Review Guidelines compliance, memory and rendering performance profiling, and Swift concurrency patterns for iOS.
engineering iosswiftswiftuiuikitcore-datamobileWhat is ios-swift?
Expert iOS development skill covering SwiftUI, UIKit, Core Data, App Store guidelines, and performance optimization. Use this skill when building, reviewing, or debugging iOS apps - views, navigation, data persistence, animations, or submission preparation. Triggers on SwiftUI layout and state management, UIKit view controller lifecycle, Core Data model design and migrations, App Store Review Guidelines compliance, memory and rendering performance profiling, and Swift concurrency patterns for iOS.
ios-swift
ios-swift is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex, and 1 more. Expert iOS development skill covering SwiftUI, UIKit, Core Data, App Store guidelines, and performance optimization. Use this skill when building, reviewing, or debugging iOS apps - views, navigation, data persistence, animations, or submission preparation.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex, mcp |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill ios-swift- The ios-swift skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
A senior iOS engineering skill that encodes deep expertise in building production-quality iOS applications with Swift. It covers the full iOS development spectrum - from SwiftUI declarative interfaces and UIKit imperative patterns to Core Data persistence, App Store submission compliance, and runtime performance optimization. The skill prioritizes modern Swift idioms (async/await, structured concurrency, property wrappers) while maintaining practical UIKit knowledge for legacy and hybrid codebases. Apple's platform is the foundation - lean on system frameworks before reaching for third-party dependencies.
Tags
ios swift swiftui uikit core-data mobile
Platforms
- claude-code
- gemini-cli
- openai-codex
- mcp
Related Skills
Pair ios-swift with these complementary skills:
Frequently Asked Questions
What is ios-swift?
Expert iOS development skill covering SwiftUI, UIKit, Core Data, App Store guidelines, and performance optimization. Use this skill when building, reviewing, or debugging iOS apps - views, navigation, data persistence, animations, or submission preparation. Triggers on SwiftUI layout and state management, UIKit view controller lifecycle, Core Data model design and migrations, App Store Review Guidelines compliance, memory and rendering performance profiling, and Swift concurrency patterns for iOS.
How do I install ios-swift?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill ios-swift in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support ios-swift?
This skill works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
iOS Swift Development
A senior iOS engineering skill that encodes deep expertise in building production-quality iOS applications with Swift. It covers the full iOS development spectrum - from SwiftUI declarative interfaces and UIKit imperative patterns to Core Data persistence, App Store submission compliance, and runtime performance optimization. The skill prioritizes modern Swift idioms (async/await, structured concurrency, property wrappers) while maintaining practical UIKit knowledge for legacy and hybrid codebases. Apple's platform is the foundation - lean on system frameworks before reaching for third-party dependencies.
When to use this skill
Trigger this skill when the user:
- Asks to build, review, or debug SwiftUI views, modifiers, or navigation
- Needs help with UIKit view controllers, Auto Layout, or table/collection views
- Wants to design or query a Core Data model, handle migrations, or debug persistence
- Asks about App Store Review Guidelines, metadata, or submission requirements
- Needs to profile and fix memory leaks, rendering hitches, or energy usage
- Is working with Swift concurrency (async/await, actors, TaskGroups) in an iOS context
- Wants to implement animations, gestures, or custom drawing on iOS
- Asks about integrating SwiftUI and UIKit in the same project
Do NOT trigger this skill for:
- General Swift language questions with no iOS/Apple platform context
- macOS-only, watchOS-only, or server-side Swift development
Key principles
Declarative first, imperative when necessary - Use SwiftUI for new screens and features. Fall back to UIKit only when SwiftUI lacks the capability (complex collection layouts, certain UIKit-only APIs) or when integrating into a legacy codebase. Mix via
UIHostingControllerandUIViewRepresentablewhen needed.The system is your design library - Use SF Symbols, system fonts (
.body,.title), standard colors (.primary,.secondary), and built-in controls before custom implementations. System components get Dark Mode, Dynamic Type, and accessibility for free.State drives the UI, not the other way around - In SwiftUI, the view is a function of state. Pick the right property wrapper (
@State,@Binding,@StateObject,@EnvironmentObject,@Observable) based on ownership and scope. In UIKit, keep view controllers thin by moving state logic into separate models.Measure with Instruments, not intuition - Use Xcode Instruments (Time Profiler, Allocations, Core Animation, Energy Log) before optimizing. Profile on real devices - Simulator performance is not representative. An unmeasured optimization is just added complexity.
Design for App Review from day one - Follow Apple's Human Interface Guidelines and App Store Review Guidelines throughout development, not as a last-minute checklist. Rejections cost weeks. Privacy declarations (App Tracking Transparency, purpose strings), in-app purchase rules, and content policies should be architecture decisions, not afterthoughts.
Core concepts
iOS development centers on four pillars: UI frameworks (SwiftUI and UIKit), data persistence (Core Data, SwiftData, UserDefaults), system integration (notifications, background tasks, permissions), and distribution (App Store submission, TestFlight, signing).
SwiftUI is Apple's declarative UI framework. Views are value types (structs) that declare what the UI looks like for a given state. The framework diffs the view tree and applies minimal updates. State management flows through property wrappers: @State for local, @Binding for child references, @StateObject/@ObservedObject for reference-type models, and @Environment for system-provided values. With the Observation framework (@Observable), SwiftUI tracks property access at the view level for fine-grained updates.
UIKit is the imperative predecessor - view controllers manage view lifecycles (viewDidLoad, viewWillAppear, viewDidLayoutSubviews), and Auto Layout constrains positions. UIKit remains essential for UICollectionViewCompositionalLayout, advanced text editing, and existing large codebases.
Core Data is Apple's object graph and persistence framework. It manages an in-memory object graph backed by SQLite (or other stores). The stack consists of NSPersistentContainer -> NSManagedObjectContext -> NSManagedObject. Contexts are not thread-safe - use perform {} blocks and separate contexts for background work.
App Store distribution requires provisioning profiles, code signing, metadata (screenshots, descriptions, privacy labels), and compliance with App Store Review Guidelines. TestFlight enables beta testing with up to 10,000 external testers.
Common tasks
1. Build a SwiftUI list with navigation
Create a list that navigates to a detail view. Use NavigationStack (iOS 16+) for type-safe, value-based navigation.
struct ItemListView: View {
@State private var items: [Item] = Item.samples
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
.navigationTitle("Items")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
}Avoid the deprecated
NavigationViewandNavigationLink(destination:)patterns in new code.NavigationStacksupports programmatic navigation and deep linking.
2. Set up a Core Data stack with background saving
Initialize NSPersistentContainer and perform writes on a background context to keep the main thread responsive.
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { _, error in
if let error { fatalError("Core Data load failed: \(error)") }
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
func save(block: @escaping (NSManagedObjectContext) -> Void) {
let context = container.newBackgroundContext()
context.perform {
block(context)
if context.hasChanges {
try? context.save()
}
}
}
}Never perform writes on
viewContextfor large operations - it blocks the main thread. Always usenewBackgroundContext()orperformBackgroundTask.
3. Bridge SwiftUI and UIKit
Wrap a UIKit view for use in SwiftUI with UIViewRepresentable, or host SwiftUI inside UIKit with UIHostingController.
// UIKit view in SwiftUI
struct MapViewWrapper: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewWrapper
init(_ parent: MapViewWrapper) { self.parent = parent }
}
}// SwiftUI view in UIKit
let hostingController = UIHostingController(rootView: MySwiftUIView())
navigationController?.pushViewController(hostingController, animated: true)4. Profile and fix memory leaks
Use Instruments Allocations and Leaks to find retain cycles. The most common iOS memory leak is a strong reference cycle in closures.
Checklist:
- Run the Leaks instrument on a real device while exercising the suspected screen
- Check for closures capturing
selfstrongly - use[weak self]in escaping closures - Verify delegates are declared
weak(e.g.,weak var delegate: MyDelegate?) - Look for
NotificationCenterobservers not removed ondeinit - Check
Timerinstances -Timer.scheduledTimerretains its target - In SwiftUI, verify
@StateObjectis used for creation,@ObservedObjectfor injection
Use the Debug Memory Graph in Xcode (Runtime -> Debug Memory Graph) for a visual view of retain cycles without launching Instruments.
5. Handle App Store submission requirements
Prepare an app for App Store Review compliance.
Checklist:
- Add all required
Info.plistpurpose strings for permissions (camera, location, photos, microphone, etc.) - Implement App Tracking Transparency (
ATTrackingManager.requestTrackingAuthorization) before any tracking - Complete the App Privacy section in App Store Connect - declare all data collected
- Use StoreKit 2 for in-app purchases; never process payments outside Apple's system for digital goods
- Ensure login-based apps provide Sign in with Apple alongside other third-party login options
- Provide a "Restore Purchases" button if the app offers non-consumable IAPs or subscriptions
- Include a privacy policy URL accessible from both the app and App Store listing
- Test on the minimum supported iOS version declared in your deployment target
Load
references/app-store-guidelines.mdfor the full Review Guidelines checklist and common rejection reasons.
6. Optimize SwiftUI rendering performance
Reduce unnecessary view re-evaluations and layout passes.
Rules:
- Mark view models with
@Observable(iOS 17+) for fine-grained tracking instead ofObservableObject - Extract expensive subviews into separate structs so SwiftUI can skip re-evaluation
- Use
EquatableViewor conform views toEquatableto control diffing - Prefer
LazyVStack/LazyHStackinsideScrollViewfor large lists - Avoid
.id()modifier changes that destroy and recreate views - Use
task {}instead ofonAppearfor async work - it cancels automatically
// Bad: entire body re-evaluates when unrelated state changes
struct BadView: View {
@ObservedObject var model: LargeModel
var body: some View {
VStack {
Text(model.title)
ExpensiveChart(data: model.chartData) // re-evaluated even if chartData unchanged
}
}
}
// Good: extracted subview only re-evaluates when its input changes
struct GoodView: View {
@State var model = LargeModel() // @Observable macro
var body: some View {
VStack {
Text(model.title)
ChartView(data: model.chartData)
}
}
}7. Implement structured concurrency for networking
Use Swift's async/await with proper task management for iOS networking.
class ItemService {
private let session: URLSession
private let decoder = JSONDecoder()
init(session: URLSession = .shared) {
self.session = session
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
func fetchItems() async throws -> [Item] {
let url = URL(string: "https://api.example.com/items")!
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
return try decoder.decode([Item].self, from: data)
}
}
// In SwiftUI
struct ItemListView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task {
do {
items = try await ItemService().fetchItems()
} catch {
// handle error
}
}
}
}Use
.task {}in SwiftUI - it runs when the view appears, cancels when it disappears, and restarts if the view identity changes. Never useTask {}insideonAppearwithout manual cancellation.
Anti-patterns / common mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Force unwrapping optionals | Crashes at runtime with no recovery path | Use guard let, if let, or nil-coalescing ?? |
| Writing to Core Data on the main context | Blocks the main thread during saves, causes UI hitches | Use newBackgroundContext() with perform {} |
| Massive view controllers | UIKit VCs with 1000+ lines become unmaintainable | Extract logic into view models, coordinators, or child VCs |
| Strong self in escaping closures | Creates retain cycles and memory leaks | Use [weak self] in escaping closures, [unowned self] only when lifetime is guaranteed |
| Ignoring the main actor | Updating UI from background threads causes undefined behavior | Use @MainActor annotation or MainActor.run {} for UI updates |
| Hardcoded strings and colors | Breaks localization and Dark Mode | Use LocalizedStringKey, asset catalog colors, and semantic system colors |
Skipping LazyVStack for long lists |
Eager VStack in ScrollView instantiates all views at once |
Use LazyVStack or List for scrollable content with many items |
| Storing images in Core Data | Bloats the SQLite store, slows fetches | Store image data on disk, keep file paths in Core Data; use allowsExternalBinaryDataStorage for large blobs |
| Testing on Simulator only | Simulator does not reflect real device performance, memory, or thermal behavior | Always profile and test on physical devices before submission |
| Skipping privacy purpose strings | Automatic App Store rejection | Add NSCameraUsageDescription, NSLocationWhenInUseUsageDescription, etc. for every permission |
Gotchas
@StateObjectvs@ObservedObjecton the wrong owner causes views to reset - Using@ObservedObjectto create a view model (instead of injecting one) means SwiftUI may recreate the object every time the parent view re-renders, destroying all state. Use@StateObjectwhen the view owns the object's lifecycle; use@ObservedObjectonly when the object is injected from outside.Core Data
NSManagedObjectContextis not thread-safe and crashes are non-obvious - Accessing a managed object or its context from any thread other than the one it was created on causes data corruption or crashes that appear intermittent. Always usecontext.perform {}for background context work, and never passNSManagedObjectinstances across threads - pass object IDs instead.App Store rejection for missing purpose strings is instant and takes days to resolve - If your app accesses camera, photos, location, microphone, contacts, or any other private data without a corresponding
NS*UsageDescriptionkey inInfo.plist, Apple rejects the binary automatically within hours of submission. AuditInfo.plistagainst your permission calls before every submission, not just the first one.NavigationViewis deprecated but mixing it withNavigationStackbreaks navigation state - In Xcode projects with mixed iOS version support, usingNavigationViewon older iOS alongsideNavigationStackon iOS 16+ causes navigation state corruption. Pick one per navigation hierarchy - useNavigationStackwith availability checks for older OS rather than mixing both.Storing large blobs in Core Data's SQLite store bloats the database and slows all fetches - SQLite stores all column data in the same file. Even one row with a 5MB image makes every fetch of that entity slow because SQLite reads past the image data. Store binary assets on disk via FileManager, keep only the file path in Core Data, and use
allowsExternalBinaryDataStoragefor smaller blobs that Apple should manage externally.
References
For detailed guidance on specific iOS topics, load the relevant reference file:
references/swiftui-patterns.md- Navigation patterns, state management deep dive, custom modifiers, animations, and accessibility in SwiftUIreferences/uikit-patterns.md- View controller lifecycle, Auto Layout best practices, collection view compositional layouts, and coordinator patternreferences/core-data-guide.md- Model design, relationships, fetch request optimization, migrations, and CloudKit syncreferences/app-store-guidelines.md- Review Guidelines checklist, common rejection reasons, privacy requirements, and in-app purchase rulesreferences/performance-tuning.md- Instruments workflows, memory profiling, rendering optimization, energy efficiency, and launch time reduction
Only load a reference file when the current task requires that depth - they are detailed and will consume context.
References
app-store-guidelines.md
App Store Guidelines
Pre-Submission Checklist
Privacy and permissions
- Add
NSCameraUsageDescriptionto Info.plist if using camera - Add
NSPhotoLibraryUsageDescriptionif accessing photo library - Add
NSLocationWhenInUseUsageDescriptionorNSLocationAlwaysAndWhenInUseUsageDescriptionfor location - Add
NSMicrophoneUsageDescriptionif recording audio - Add
NSContactsUsageDescriptionif accessing contacts - Add
NSCalendarsUsageDescriptionif accessing calendar - Add
NSUserTrackingUsageDescriptionfor App Tracking Transparency - All purpose strings must clearly explain WHY the permission is needed (not just "we need camera access")
- Implement
ATTrackingManager.requestTrackingAuthorizationBEFORE any tracking or ad SDK initialization - Complete the App Privacy section in App Store Connect (data collection declarations)
Authentication
- If the app offers third-party login (Google, Facebook), also offer Sign in with Apple
- Sign in with Apple must be presented as a prominently displayed option
- Guest access should be available where reasonable (content browsing)
- Login-required apps must provide demo credentials in the review notes
In-App Purchases
- All digital goods and services must use Apple's In-App Purchase system
- Physical goods and services (Uber rides, food delivery) may use external payment
- Provide a "Restore Purchases" button for non-consumable IAPs and subscriptions
- Clearly display subscription terms, pricing, and renewal info before purchase
- Use StoreKit 2 for new implementations
- Handle receipt validation server-side for production apps
- Never reference external purchasing methods for digital content
Content and behavior
- App must function as described in metadata - no hidden features
- No placeholder content (lorem ipsum, test data, dummy screenshots)
- App must be fully functional without requiring additional purchases to be usable
- User-generated content apps must have reporting and blocking mechanisms
- Apps with age-restricted content must implement age gating
- No misleading screenshots or app previews
Technical requirements
- Runs without crashing on the declared minimum iOS version
- Supports all screen sizes for the declared device families (iPhone, iPad)
- IPv6-only network compatibility (no hardcoded IPv4 addresses)
- Does not use private APIs or undocumented frameworks
- App size is reasonable (keep under 200MB for cellular downloads)
- Background modes declared in Info.plist match actual background usage
- No excessive battery, memory, or CPU usage
Metadata
- App name does not include generic terms like "best", "#1", or competitor names
- Description accurately reflects app functionality
- Keywords are relevant (no competitor names or misleading terms)
- Screenshots show actual app UI (no marketing overlays that obscure functionality)
- Privacy policy URL is valid and accessible
- Support URL is valid and leads to a support mechanism
- Age rating is set correctly based on content
Common Rejection Reasons
Guideline 2.1 - Performance: App Completeness
Cause: App crashes, has placeholder content, or features don't work.
Fix:
- Test every flow on the minimum supported iOS version on a real device
- Remove all debug/test UI and placeholder content
- Ensure all buttons and links lead somewhere functional
Guideline 2.3.3 - Performance: Screenshots
Cause: Screenshots don't match the actual app or show a device frame with wrong device type.
Fix:
- Use actual app screenshots taken on the current UI
- Match screenshot device to the upload slot (iPhone 6.7" for iPhone 15 Pro Max, etc.)
Guideline 3.1.1 - Business: In-App Purchase
Cause: Using external payment for digital goods or not offering IAP for premium features.
Fix:
- All digital content, subscriptions, and premium features must use StoreKit
- Physical goods/services are exempt
Guideline 4.0 - Design: Minimum Functionality
Cause: App is too simple, wraps a website, or is a trivial modification of a template.
Fix:
- Add native functionality beyond what a web app provides
- Include meaningful features that justify a native app
Guideline 5.1.1 - Legal: Privacy - Data Collection
Cause: Missing privacy purpose strings, tracking without ATT prompt, or incomplete privacy declarations.
Fix:
- Add descriptive purpose strings for every permission
- Request ATT before any tracking
- Complete all data collection declarations in App Store Connect
Guideline 5.1.2 - Legal: Privacy - Data Use and Sharing
Cause: App collects data not disclosed in the privacy label, or shares data without consent.
Fix:
- Audit all SDKs and analytics for data collection
- Declare everything in the privacy nutrition labels
- Provide opt-out mechanisms where applicable
TestFlight
Internal testing
- Up to 100 internal testers (members of your App Store Connect team)
- No review required
- Builds available immediately after processing
External testing
- Up to 10,000 external testers
- First build of each version requires Beta App Review (usually < 24 hours)
- Subsequent builds of the same version are usually auto-approved
- Testers join via public link or email invitation
- Builds expire after 90 days
Best practices
- Include meaningful "What to Test" notes for each build
- Use test groups to segment testers by feature or risk level
- Monitor crash reports and feedback in App Store Connect
- Remove testers who are not actively testing
App Store Connect Submission Flow
- Archive the app in Xcode (Product -> Archive)
- Upload via Xcode Organizer or
xcodebuild -exportArchive - Configure in App Store Connect: screenshots, description, keywords, pricing
- Submit for review - typical review time is 24-48 hours
- Respond to any rejection with specific fixes in the Resolution Center
Expedited review
Available for critical bug fixes. Request through the Apple developer contact form. Include:
- Detailed description of the critical issue
- Steps to reproduce
- Business impact explanation
- Expected timeline for fix
Use sparingly - Apple tracks expedited review requests and may deny frequent requesters.
core-data-guide.md
Core Data Guide
Stack Architecture
NSPersistentContainer
|
+-- NSPersistentStoreCoordinator
| |
| +-- NSPersistentStore (SQLite, In-Memory, etc.)
|
+-- viewContext (NSManagedObjectContext - main queue)
|
+-- newBackgroundContext() (NSManagedObjectContext - private queue)Standard setup
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "DataModel")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { description, error in
if let error {
fatalError("Core Data store failed to load: \(error.localizedDescription)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}Model Design
Entity attributes
| Swift type | Core Data type | Notes |
|---|---|---|
String |
String | |
Int16/32/64 |
Integer 16/32/64 | Use Int64 for most cases |
Double |
Double | |
Bool |
Boolean | |
Date |
Date | |
Data |
Binary Data | Enable "Allows External Storage" for > 100KB |
UUID |
UUID | Good for unique identifiers |
URL |
URI | Stored as string internally |
Relationships
- To-one: Optional by default; set delete rule appropriately
- To-many: Returns
NSSet; use generated accessors orNSOrderedSet - Always set inverse relationships - Core Data uses them for graph consistency
Delete rules
| Rule | Behavior |
|---|---|
| Nullify | Set inverse to nil (default, safest) |
| Cascade | Delete related objects (parent deletes children) |
| Deny | Prevent deletion if relationships exist |
| No Action | Do nothing (can leave orphans - avoid) |
Fetch Requests
Basic fetch
let request = NSFetchRequest<Item>(entityName: "Item")
request.predicate = NSPredicate(format: "isCompleted == %@", NSNumber(value: false))
request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.createdAt, ascending: false)]
request.fetchLimit = 50
let items = try context.fetch(request)Common predicates
// String matching
NSPredicate(format: "name CONTAINS[cd] %@", searchText)
// Date range
NSPredicate(format: "createdAt >= %@ AND createdAt <= %@", startDate as CVarArg, endDate as CVarArg)
// Relationship
NSPredicate(format: "category.name == %@", "Books")
// In set
NSPredicate(format: "status IN %@", ["active", "pending"])
// Compound
NSCompoundPredicate(andPredicateWithSubpredicates: [predicate1, predicate2])Performance optimization
- Use
fetchBatchSize(typically 20-50) to reduce memory for large result sets - Use
propertiesToFetchto load only needed attributes (partial faulting) - Use
NSAsynchronousFetchRequestfor fetches that might take > 100ms - Use
NSFetchedResultsControllerfor table/collection view data sources - Avoid fetching in loops - batch operations instead
request.fetchBatchSize = 20
request.propertiesToFetch = ["name", "createdAt"]
request.returnsObjectsAsFaults = true // default, loads data on access@FetchRequest in SwiftUI
struct ItemListView: View {
@FetchRequest(
sortDescriptors: [SortDescriptor(\.createdAt, order: .reverse)],
predicate: NSPredicate(format: "isCompleted == false"),
animation: .default
)
private var items: FetchedResults<Item>
var body: some View {
List(items) { item in
Text(item.name ?? "Untitled")
}
}
}Background Operations
Writing on a background context
func importItems(_ data: [ItemData]) {
let context = PersistenceController.shared.container.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.perform {
for itemData in data {
let item = Item(context: context)
item.name = itemData.name
item.createdAt = Date()
}
do {
try context.save()
} catch {
context.rollback()
print("Background save failed: \(error)")
}
}
}Batch operations (iOS 13+)
For large imports/updates, use batch operations that bypass the object graph:
// Batch insert
let batchInsert = NSBatchInsertRequest(entityName: "Item",
managedObjectHandler: { object in
guard let item = object as? Item else { return true }
// configure item from your data source
return false // return true when done
})
batchInsert.resultType = .objectIDs
let result = try context.execute(batchInsert) as? NSBatchInsertResult
// Merge changes into viewContext
if let objectIDs = result?.result as? [NSManagedObjectID] {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSInsertedObjectsKey: objectIDs],
into: [container.viewContext]
)
}
// Batch delete
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
fetchRequest.predicate = NSPredicate(format: "isArchived == true")
let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)
batchDelete.resultType = .resultTypeObjectIDs
try context.execute(batchDelete)Thread Safety Rules
- Never pass
NSManagedObjectacross threads - useobjectIDand re-fetch - Always use
perform {}orperformAndWait {}for context operations viewContextis main-queue only - access only from the main thread- Background contexts are private-queue - only access inside
perform {} - Set
automaticallyMergesChangesFromParent = trueonviewContextto see background saves
// Safe: pass objectID across threads
let objectID = item.objectID
context.perform {
let item = context.object(with: objectID) as! Item
item.name = "Updated"
try? context.save()
}Migrations
Lightweight migration (automatic)
Core Data handles these automatically when you add the migration options:
let description = NSPersistentStoreDescription()
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = trueSupported lightweight changes:
- Add/remove attributes (with default values for non-optional new attributes)
- Add/remove entities
- Rename entities/attributes (set renaming identifier in model editor)
- Change relationship cardinality
- Add/remove relationships
Custom migration
For changes that exceed lightweight migration (transforming data, splitting entities):
- Create a new model version in Xcode (Editor -> Add Model Version)
- Create an
NSMappingModelwith customNSEntityMigrationPolicysubclasses - Implement
createDestinationInstances(forSource:in:manager:)for data transformation
CloudKit Sync (NSPersistentCloudKitContainer)
let container = NSPersistentCloudKitContainer(name: "DataModel")
let description = container.persistentStoreDescriptions.first!
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.yourapp.container"
)
container.loadPersistentStores { _, error in
if let error { fatalError("CloudKit store failed: \(error)") }
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicyKey considerations:
- All attributes must be optional (CloudKit requirement)
- Unique constraints are not supported with CloudKit
- Relationships must have inverses
- Monitor sync status with
NSPersistentCloudKitContainer.eventChangedNotification - Test with real iCloud accounts on devices, not Simulator
performance-tuning.md
Performance Tuning
Instruments Workflows
Time Profiler
Use to find CPU bottlenecks - functions consuming the most time.
- Profile on a real device (Product -> Profile or Cmd+I)
- Select the Time Profiler template
- Record while reproducing the slow behavior
- Sort by "Self Weight" to find hotspots
- Uncheck "Separate by Thread" to see overall impact
- Check "Invert Call Tree" to see leaf functions first
- Double-click to jump to source code
Common findings:
- JSON decoding on the main thread
- Image resizing/decoding synchronously
- Complex view layout calculations
- String processing in tight loops
Allocations
Use to find memory issues - objects that grow without bound.
- Select the Allocations template
- Record while exercising the suspected flow
- Use "Mark Generation" between navigation actions
- Growth between generations reveals leaks or unbounded caches
- Filter by your app's class names to focus on your code
Look for:
- Generations that only grow (never shrink) - memory leak
- Large image allocations not released after dismissal
- Retained view controllers after navigation back
Leaks
Use alongside Allocations to find retain cycles.
- Select the Leaks template
- Exercise the app (push/pop screens, open/close features)
- Leaks instrument will flag leaked objects
- Inspect the retain cycle in the cycle graph
Common retain cycles:
- Closure capturing
selfstrongly - Delegate not declared as
weak Timerretaining its targetNotificationCenterobservers with strong references
Core Animation (Rendering)
Use to find rendering performance issues (dropped frames, hitches).
- Select the Core Animation template (or use "Animation Hitches" in newer Xcode)
- Look for frames exceeding 16.67ms (60fps) or 8.33ms (120fps on ProMotion)
- Enable "Color Blended Layers" - red areas indicate alpha blending overhead
- Enable "Color Offscreen-Rendered" - yellow areas indicate expensive offscreen passes
Common fixes:
- Set
layer.shouldRasterize = truefor complex static views (with correctrasterizationScale) - Avoid
clipsToBounds+cornerRadiuson frequently redrawn views (use a mask layer instead) - Pre-decode images to the display size with
UIGraphicsImageRenderer - Use opaque backgrounds where possible to reduce blending
Memory Optimization
Image handling
Images are the largest memory consumers in most iOS apps.
// Bad: loads full-resolution image into memory
let image = UIImage(named: "hero")
// Good: downsample to display size
func downsample(imageAt url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
]
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
else { return nil }
return UIImage(cgImage: cgImage)
}Memory budget guidelines
| Device | Safe memory ceiling |
|---|---|
| 2GB RAM (iPhone SE 3) | ~600MB |
| 4GB RAM (iPhone 14) | ~1.2GB |
| 6GB RAM (iPhone 15 Pro) | ~2GB |
Exceeding these thresholds risks jetsam termination (system kills your app). Monitor with os_proc_available_memory().
Autorelease pool optimization
// When creating many temporary objects in a loop
for imageURL in urls {
autoreleasepool {
let image = processImage(at: imageURL)
saveProcessedImage(image)
}
}Launch Time Optimization
Target: < 400ms for warm launch. Apple considers > 2 seconds unacceptable.
Measurement
// In your app delegate or @main struct
// Xcode shows pre-main time in the environment variable
// DYLD_PRINT_STATISTICS = 1Use the App Launch template in Instruments for detailed breakdown.
Pre-main optimizations
- Reduce embedded frameworks (each framework adds ~10-30ms)
- Remove unused code and frameworks
- Avoid
+loadand+initializemethods in Objective-C code - Minimize static initializers
Post-main optimizations
- Defer non-essential work past first frame render
- Load Core Data store asynchronously
- Defer analytics SDK initialization
- Use lazy initialization for heavy objects
- Prefetch critical data but don't block the main thread
@main
struct MyApp: App {
init() {
// Only essential setup here
configureAppearance()
}
var body: some Scene {
WindowGroup {
ContentView()
.task {
// Deferred initialization
await initializeAnalytics()
await prefetchUserData()
}
}
}
}SwiftUI Performance
Reducing view re-evaluations
// Use @Observable for fine-grained updates (iOS 17+)
@Observable
class ViewModel {
var title = ""
var items: [Item] = []
var isLoading = false
}
// Only views reading `title` re-evaluate when title changes
// Views reading `items` are unaffectedLazy containers
// Always use Lazy variants for large collections
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ItemRow(item: item)
}
}
}Identifying views correctly
// Bad: using index as id causes full re-render on changes
ForEach(Array(items.enumerated()), id: \.offset) { ... }
// Good: use stable identifier
ForEach(items) { item in // Item conforms to Identifiable
ItemRow(item: item)
}Avoiding unnecessary work
// Cache formatted values
struct ItemRow: View {
let item: Item
// Computed once per evaluation, not per render
private var formattedPrice: String {
item.price.formatted(.currency(code: "USD"))
}
var body: some View {
Text(formattedPrice)
}
}Energy Efficiency
Battery drain causes negative App Store reviews. Profile with the Energy Log instrument.
Best practices
- Use
BGTaskSchedulerfor background work, not continuous background execution - Batch network requests instead of many small calls
- Stop location updates when not needed (
locationManager.stopUpdatingLocation()) - Use significant location changes (
startMonitoringSignificantLocationChanges()) when precise tracking is unnecessary - Reduce GPS accuracy when high precision is not required
- Stop timers when views disappear
- Use
URLSessionbackground transfers for large downloads - Avoid polling - use push notifications or server-sent events
Networking efficiency
// Batch requests with TaskGroup
func fetchAllData() async throws -> (users: [User], posts: [Post]) {
async let users = fetchUsers()
async let posts = fetchPosts()
return try await (users, posts)
}
// Use URLCache effectively
let config = URLSessionConfiguration.default
config.urlCache = URLCache(
memoryCapacity: 20 * 1024 * 1024, // 20 MB memory
diskCapacity: 100 * 1024 * 1024 // 100 MB disk
)
config.requestCachePolicy = .returnCacheDataElseLoad swiftui-patterns.md
SwiftUI Patterns
Navigation (iOS 16+)
NavigationStack
NavigationStack replaces NavigationView and supports value-based, programmatic navigation.
@State private var path = NavigationPath()
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
// Programmatic navigation
path.append(someItem) // push
path.removeLast() // pop
path = NavigationPath() // pop to rootTabView
TabView {
Tab("Home", systemImage: "house") {
HomeView()
}
Tab("Settings", systemImage: "gear") {
SettingsView()
}
}Sheet and fullScreenCover
@State private var showSheet = false
Button("Show") { showSheet = true }
.sheet(isPresented: $showSheet) {
DetailView()
}
// For item-based presentation
.sheet(item: $selectedItem) { item in
DetailView(item: item)
}State Management
Property wrapper decision tree
| Wrapper | Ownership | Use when |
|---|---|---|
@State |
View owns it | Simple local value types (strings, booleans, enums) |
@Binding |
Parent owns it | Child view needs read/write access to parent's state |
@StateObject |
View creates it | View creates and owns a reference-type model |
@ObservedObject |
Parent passes it | View receives but does not own a reference-type model |
@EnvironmentObject |
Ancestor injects it | Shared model needed by many views in the hierarchy |
@Environment |
System provides it | System values: color scheme, locale, dismiss action |
@Observable (macro) |
Any | iOS 17+: replaces ObservableObject with fine-grained tracking |
@Observable (iOS 17+)
@Observable
class UserModel {
var name: String = ""
var email: String = ""
var avatarURL: URL?
}
struct ProfileView: View {
@State var model = UserModel() // Note: @State, not @StateObject
var body: some View {
VStack {
Text(model.name) // Only re-renders when name changes
Text(model.email) // Only re-renders when email changes
}
}
}The @Observable macro tracks property access per-view, so changing email does not re-evaluate views that only read name. This is a major performance improvement over ObservableObject which notifies all subscribers on any property change.
@StateObject vs @ObservedObject
// Parent creates the model - use @StateObject
struct ParentView: View {
@StateObject private var viewModel = ItemViewModel()
var body: some View {
ChildView(viewModel: viewModel)
}
}
// Child receives the model - use @ObservedObject
struct ChildView: View {
@ObservedObject var viewModel: ItemViewModel
var body: some View {
Text(viewModel.title)
}
}Critical rule: @StateObject persists across view re-evaluations. @ObservedObject does not - if the parent view's body re-evaluates, a new instance is created. Using @ObservedObject where @StateObject is needed causes state loss.
Custom Modifiers
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 2)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardStyle())
}
}
// Usage
Text("Hello").cardStyle()Conditional modifiers (avoid the naive approach)
// Bad: changes view identity, destroys state
if condition {
text.bold()
} else {
text
}
// Good: preserves view identity
text.bold(condition) // For built-in modifiers that accept a boolean
// For custom conditional styling
text.opacity(condition ? 1.0 : 0.5)Animations
Implicit animations
@State private var isExpanded = false
VStack {
RoundedRectangle(cornerRadius: 12)
.frame(height: isExpanded ? 300 : 100)
.animation(.spring(duration: 0.4), value: isExpanded)
}Explicit animations
withAnimation(.easeInOut(duration: 0.3)) {
isExpanded.toggle()
}Transitions
if showDetail {
DetailView()
.transition(.move(edge: .bottom).combined(with: .opacity))
}Phase animator (iOS 17+)
PhaseAnimator([false, true]) { phase in
Image(systemName: "heart.fill")
.scaleEffect(phase ? 1.2 : 1.0)
.foregroundStyle(phase ? .red : .pink)
}Accessibility in SwiftUI
// Labels
Image(systemName: "heart.fill")
.accessibilityLabel("Favorite")
// Actions
Button("Delete") { delete() }
.accessibilityHint("Removes the item permanently")
// Grouping
HStack {
Image(systemName: "star.fill")
Text("4.5 out of 5 stars")
}
.accessibilityElement(children: .combine)
// Custom actions
Text(item.title)
.accessibilityAction(named: "Delete") { delete(item) }
.accessibilityAction(named: "Share") { share(item) }
// Traits
Text("Breaking News")
.accessibilityAddTraits(.isHeader)
// Reduced motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
withAnimation(reduceMotion ? .none : .spring()) {
isExpanded.toggle()
}Common SwiftUI pitfalls
| Pitfall | Fix |
|---|---|
| View not updating | Verify correct property wrapper; @ObservedObject does not persist on re-evaluation |
| Sheet not dismissing | Use @Environment(\.dismiss) var dismiss then call dismiss() |
| List performance | Use List (built-in lazy) or LazyVStack in ScrollView for 100+ items |
| Dark mode broken | Use semantic colors (.primary, .secondary) and asset catalog colors |
| Keyboard not dismissing | Add .scrollDismissesKeyboard(.interactively) or use FocusState |
| Preview crashes | Provide mock data; never hit real network or Core Data in previews |
uikit-patterns.md
UIKit Patterns
View Controller Lifecycle
Methods are called in this order:
init(coder:) or init(nibName:bundle:)
-> loadView()
-> viewDidLoad() // one-time setup: add subviews, constraints
-> viewWillAppear(_:) // about to appear: refresh data, start animations
-> viewIsAppearing(_:) // iOS 13+: view has traits and geometry
-> viewDidAppear(_:) // fully visible: start timers, analytics
-> viewWillDisappear(_:) // about to leave: pause media, save draft
-> viewDidDisappear(_:) // fully gone: cancel network, release resources
-> deinit // verify this is called (no retain cycles)Common mistakes
- Doing layout work in
viewDidLoadwhen geometry is not yet final - useviewDidLayoutSubviewsorviewIsAppearing - Not calling
superfor lifecycle methods - Putting one-time setup in
viewWillAppear(called every time the view appears)
Auto Layout
Programmatic constraints
class CustomViewController: UIViewController {
private let titleLabel = UILabel()
private let actionButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
private func setupViews() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
actionButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(titleLabel)
view.addSubview(actionButton)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
actionButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 24),
actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
}
}Key rules
- Always set
translatesAutoresizingMaskIntoConstraints = falsefor programmatic constraints - Use
safeAreaLayoutGuidefor top/bottom to avoid notch and home indicator - Use
layoutMarginsGuidefor consistent horizontal padding - Activate constraints in batches with
NSLayoutConstraint.activate([])for performance - Set content hugging and compression resistance priorities to resolve ambiguity
Intrinsic content size priorities
// Label should not stretch beyond its text
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
// Label should not be compressed
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)UICollectionView Compositional Layout
The modern way to build complex collection views (iOS 13+).
func createLayout() -> UICollectionViewLayout {
// Item
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
// Group
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(200)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
// Section
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
// Header
let headerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)
)
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top
)
section.boundarySupplementaryItems = [header]
return UICollectionViewCompositionalLayout(section: section)
}Diffable Data Source (iOS 13+)
var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, item in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "ItemCell",
for: indexPath
) as! ItemCell
cell.configure(with: item)
return cell
}
// Apply snapshot
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)Coordinator Pattern
Decouple navigation logic from view controllers.
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get }
func start()
}
class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = HomeViewController()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
func showDetail(for item: Item) {
let detailVC = DetailViewController(item: item)
detailVC.coordinator = self
navigationController.pushViewController(detailVC, animated: true)
}
}
class HomeViewController: UIViewController {
weak var coordinator: AppCoordinator?
func didSelectItem(_ item: Item) {
coordinator?.showDetail(for: item)
}
}UIKit + SwiftUI Interop
Hosting SwiftUI in UIKit
let swiftUIView = MySwiftUIView(model: model)
let hostingController = UIHostingController(rootView: swiftUIView)
// As a child view controller
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
hostingController.didMove(toParent: self)Table view and collection view cells with SwiftUI (iOS 16+)
cell.contentConfiguration = UIHostingConfiguration {
HStack {
Image(systemName: item.icon)
Text(item.title)
}
.padding()
}Delegate and Protocol Patterns
protocol ItemSelectionDelegate: AnyObject {
func didSelectItem(_ item: Item)
func didDeselectItem(_ item: Item)
}
class ItemListViewController: UIViewController {
weak var delegate: ItemSelectionDelegate? // Always weak to prevent retain cycles
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
delegate?.didSelectItem(item)
}
}Always declare delegates as weak var to prevent retain cycles between the delegate and the delegating object.
Frequently Asked Questions
What is ios-swift?
Expert iOS development skill covering SwiftUI, UIKit, Core Data, App Store guidelines, and performance optimization. Use this skill when building, reviewing, or debugging iOS apps - views, navigation, data persistence, animations, or submission preparation. Triggers on SwiftUI layout and state management, UIKit view controller lifecycle, Core Data model design and migrations, App Store Review Guidelines compliance, memory and rendering performance profiling, and Swift concurrency patterns for iOS.
How do I install ios-swift?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill ios-swift in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support ios-swift?
ios-swift works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.