Guidance for AI coding agents working in this iOS repository. Follow every rule below. When a rule here conflicts with a general convention, this file wins.
Localytics SDK sample app (LocalyticsDemoApp): SwiftUI iOS app under ios/, demonstrating Localytics integration (events, profiles, sessions, Places, notifications, in-app messages).
- Language: Swift only. Do not add Objective-C sources.
- UI: SwiftUI (with
UIViewRepresentablebridges where needed, e.g. WebKit). - Build: Xcode project at
ios/LocalyticsDemoApp.xcodeproj; schemeLocalyticsDemoApp. - Dependencies: Swift Package Manager only (
Localytics-swiftpm). No CocoaPods /Podfile. Never mix dependency managers. - App deployment target is iOS 16.0; Swift 5.0. Read project settings before using version-gated APIs; never hardcode targets elsewhere.
- Targets:
LocalyticsDemoApp,LocalyticsDemoAppTests,LocalyticsDemoAppUITests.
- All new code MUST be Swift.
- Use Swift idioms: value types (
struct,enum) by default, protocol conformance over inheritance,guardfor early exit, exhaustiveswitch,Resultor typedthrowsfor fallible paths. - Keep any
@objcsurface minimal and only when required for SDK/interop (e.g. app delegate / location callbacks); avoid@objcMembers. - No wildcard-style umbrella imports of internal modules; import only what is used.
- Do NOT generate comments explaining what code does. Code must be self-documenting through naming.
- Do NOT add documentation comments (
///,/** */) to private or internal members. - Do NOT add file headers, license banners,
MARK:sections in new small files, TODO markers, or decorative separators.MARK:is permitted only when editing an existing large file that already uses them. - Doc comments are permitted ONLY on public API surfaces of framework/package targets, and only when the contract is non-obvious (threading guarantees, units, ownership semantics not expressible in the type system).
- A comment is permitted ONLY to explain WHY something non-obvious exists (e.g., a workaround for an OS bug, with the radar/issue link). Never WHAT.
- If you feel a comment is needed to explain logic, refactor the code instead: extract a named function, rename a variable, simplify the branch.
- Never leave commented-out code.
Write the most memory-efficient, allocation-conscious code possible. Every allocation on a hot path is a defect.
- Avoid heap allocation in hot paths:
draw(_:),layoutSubviews,cellForRowAt/cell configuration, scroll callbacks,CADisplayLinkticks, audio/video render callbacks, and any per-frame code. Preallocate and reuse formatters, paths, buffers, and contexts as stored properties. - Prefer
structoverclassfor models; keep structs small and avoid unintended copies of large ones (passinoutor wrap in a class deliberately when mutation-in-place matters). - Exploit copy-on-write: mutate collections in place; call
reserveCapacity(_:)when the size is known. - Use
ContiguousArrayfor hot numeric arrays; useUnsafeBufferPointer/withUnsafeBytesonly where profiling justifies it and the scope is contained. - Avoid boxing: no
Any,AnyObject, orAnyHashablein hot paths; avoid protocol existentials where generics (some/<T>) allow static dispatch. - Mark classes
finalunless subclassed; useprivate/fileprivateto enable devirtualization and dead-code stripping. DateFormatter,NumberFormatter,ISO8601DateFormatter,NSRegularExpression, andJSONDecoderare expensive: create once, store, reuse. Never instantiate them per cell or per call.- Never concatenate strings with
+in loops; use a singleStringwithreserveCapacityorjoined(). - Prefer
lazy varfor expensive main-thread-only properties; avoidlazyon structs. - Avoid KVO and
NotificationCenteron hot paths; prefer direct delegation or Combine/async streams with bounded demand.
- Every closure stored by an object, passed to a long-lived API, or escaping into async work must use
[weak self]unless a strong capture is provably finite and intended; state that intent in the PR summary, not a comment. - Delegates are
weak varand the protocol is class-bound (AnyObject). Same for any back-reference in a parent-child object graph. - Break retain cycles in Combine: store cancellables in a
Set<AnyCancellable>owned by the subscriber; use[weak self]insink. - Timers (
Timer.scheduledTimer),CADisplayLink, andDispatchSourceTimerretain their targets: invalidate indeinit/teardown, or use the block-based API with[weak self]. NotificationCenterblock-based observers must be removed in teardown; store the token. Selector-based observers on iOS 9+ are auto-removed but still remove them explicitly in long-lived objects.URLSessionwith a delegate retains the delegate untilfinishTasksAndInvalidate()/invalidateAndCancel()is called; call it.
- Downsample images to display size with
ImageIO(CGImageSourceCreateThumbnailAtIndexwithkCGImageSourceThumbnailMaxPixelSize) orUIGraphicsImageRenderer; never assign full-resolutionUIImage(data:)to a thumbnail. - Use
UIImage(named:)only for bundled assets that benefit from the system cache; useUIImage(contentsOfFile:)for large one-off images to avoid cache bloat. - Cache decoded images in
NSCachewithcountLimit/totalCostLimit; respond to memory warnings (didReceiveMemoryWarning,UIApplication.didReceiveMemoryWarningNotification) by purging caches. - Prefer asset catalogs with proper scale variants; no oversized PDFs rasterized at runtime in hot paths.
- Use Swift Concurrency (
async/await, actors) for new code; annotate UI-touching code@MainActor. NoDispatchQueue.main.asyncsprinkled as a fix for data races; fix the isolation instead. - Never block the main thread: no synchronous disk, network, or Core Data fetches on main.
- Avoid unstructured
Task {}that outlives its owner; prefer structured concurrency or store and cancel the task in teardown. - Do not create dedicated dispatch queues per object; use shared concurrent queues or actors.
- Core Data: use background contexts via
perform; never pass managed objects across contexts, passNSManagedObjectID. UsefetchBatchSizeand faulting; never fetch all rows to count them.
- This app is primarily SwiftUI. Keep
bodycheap and allocation-light; extract subviews; useEquatableviews or stable identity to limit diffing; use@State/@StateObjectcorrectly (@StateObjectfor owned reference models,@ObservedObjectfor injected); preferLazyVStack/Listfor long content; avoidAnyViewand.id(UUID()). - Deployment target is iOS 16: use
ObservableObject(not the@Observablemacro, which requires iOS 17+). If the deployment target is raised later, prefer@Observablewhere allowed. - When UIKit interop is required (
UIViewRepresentable/UIViewControllerRepresentable): cell reuse is mandatory; no allocation or synchronous image decode incellForRowAt; flatten view hierarchies; setshadowPathwhen using shadows; avoid offscreen rendering on scrolling content. - CALayer: set
shouldRasterizeonly with measurement; avoid masks/shadows withoutshadowPathand corner radius + masksToBounds on scrolling content.
- Prefer static dispatch:
final,private, generics over existentials, structs over classes. - Avoid
NSObjectinheritance and dynamic dispatch in Swift unless required for interop (e.g.CLLocationManagerDelegate). - Avoid reflection (
Mirror) and string-based selectors outside interop boundaries. - Prefer
Codablewith compile-time synthesis; no runtime schema reflection libraries on hot paths.
- Follow the existing layout under
ios/LocalyticsDemoApp/(Views/,Views/ViewModel/,CustomReusableVw/,SupportingFiles/). Do not create new targets/packages without instruction. - Prefer the existing SwiftUI structure. Use ViewModels (
ObservableObject) where the project already does (e.g.PlacesVM) or where shared mutable state needs a clear owner; do not impose a full MVVM/repository layer on simple demo screens. - Views observe published state from view models when ViewModels are used. View models should not import view-only types beyond what bridging requires.
- Constructor injection only; no new singletons for stateful services beyond those already established (e.g. existing Localytics / app-delegate patterns).
All code follows the "never nester" philosophy: deep nesting is a defect, not a style preference. Maximum nesting depth is 2 (a method body is depth 0; each if/guard else/for/while/switch/closure body adds 1).
Reduce nesting using two techniques, in this order:
- Inversion: flip conditions and exit early. Validate preconditions at the top with
guard ... else { return }, so the happy path reads straight down the left margin with noelsebranches. - Extraction: when inversion is not enough, pull the nested block into a named private function. The function name replaces the comment you were tempted to write.
Rules:
- No
elseafter aguardor early return; the remaining code IS the else. - Loop bodies that need a condition use
continueat the top, not a wrappingif. - Nested closures count as nesting: a completion handler inside a completion handler inside an
ifis already over budget; flatten withasync/awaitor extract. do/catchandswitchcases with multi-branch bodies get extracted into functions.- When editing existing code that violates this, flatten the parts you touch; do not restructure untouched code.
- Swift API Design Guidelines. 4-space indent, no tabs.
- Naming: descriptive and complete.
remainingRetryCount, notcnt. Booleans read as assertions:isVisible,hasSession. - Functions do one thing. If a function needs a section comment, split it.
- No force unwraps (
!), force casts (as!), ortry!outside tests. Useguard let/if letwith a handled failure path. - Visibility: everything
privateunless it must be wider.
Run from the repository root before considering any task complete. Use this project and scheme (do not guess):
PROJECT="ios/LocalyticsDemoApp.xcodeproj"
SCHEME="LocalyticsDemoApp"
xcodebuild -list -project "$PROJECT" -json
xcodebuild build -project "$PROJECT" -scheme "$SCHEME" -destination 'generic/platform=iOS Simulator'
xcodebuild test -project "$PROJECT" -scheme "$SCHEME" -destination 'platform=iOS Simulator,name=iPhone 16'
swiftlint --strict # only if .swiftlint.yml exists at the repo root
- App deployment target is iOS 16.0: use
ObservableObject, not@Observable. Never add@availablesprinkling to work around a target mismatch without noting it in the summary. - Fix all new compiler and SwiftLint warnings you introduce; do not suppress without justification in the PR description. Treat warnings as errors in touched files.
- Add or update unit tests for every behavior change using XCTest (targets already use XCTest); no new test frameworks.
- Do not upgrade dependencies unless asked. Add dependencies only via Swift Package Manager in the Xcode project / package resolution.
Every change that touches object lifetimes (view controllers, views, closures, delegates, timers, observers, Combine subscriptions, Tasks, Core Data contexts) must be checked for leaks before the task is considered complete.
- Debug builds must assert deallocation of view controllers: if the project has a deinit-tracking utility or
LifetimeTracker-style dependency, wire new controllers into it; otherwise add adeinitassertion pattern only where the project already uses one (do not introduce a new dependency without noting it in the summary). - Verify with Xcode Instruments when the change affects caches, images, or navigation flows: run the Leaks and Allocations instruments, navigate into and out of the affected screen three times, and confirm zero leaked objects and a flat persistent-allocation baseline.
- Use Xcode's Memory Graph Debugger to confirm no destroyed view controller, view, or view model remains in the graph after dismissal; investigate any purple runtime warnings.
- Enable Malloc Stack Logging and Zombie Objects in the debug scheme when chasing over-release or use-after-free.
Perform this static review on every diff before finishing:
- Every escaping or stored closure capturing
selfuses[weak self](or a justified intentional strong capture stated in the summary). - Every delegate/back-reference property is
weak; no strong parent-child cycles in the object graph. - Every
Timer,CADisplayLink, andDispatchSourceTimeris invalidated/cancelled in teardown or uses a weak target pattern. - Every
NotificationCenterblock observer token, KVO observation, and CombineAnyCancellableis stored and released with its owner. - Every unstructured
Taskthat could outlive its owner is stored and cancelled in teardown, or uses[weak self]and tolerates owner deallocation. URLSessiondelegate-based sessions are invalidated; long-lived sessions do not retain short-lived owners.- No
UIViewController,UIView, or view model reachable from a static property, global, or singleton beyond its lifetime. - Animations and
UIViewPropertyAnimators referencing views are stopped before their host is deallocated;CAAnimationdelegates do not retain the layer's owner (they retain their delegate strongly). - Core Data managed objects are not retained past their context's lifetime; background contexts are not captured strongly by long-lived closures.
- If a leak is found in existing code while working on a task, fix it if the fix is under ~15 lines and within the touched files; otherwise report it in the summary with the retention chain from the Memory Graph Debugger.
- Any intentional retain (e.g., self-retaining operation until completion) requires justification in the PR description, never an inline comment.
- Force unwraps, force casts,
try!outside tests;fatalErrorfor recoverable conditions. DispatchQueue.global().sync, semaphores to fake synchronous APIs,sleep/usleepon main.unownedunless the lifetime relationship is structurally guaranteed and stated in the summary; default toweak.- Unbounded in-memory caches (
Dictionary/NSMutableDictionaryas cache); useNSCachewith limits. print/NSLogin release code paths; use the project'sos.Logger/logging facade with appropriate privacy levels.- Hardcoded user-facing strings (use
String(localized:)/Localizable.strings), hardcoded colors and spacing (use asset catalog colors and design tokens). - Swizzling, private API use, and runtime class manipulation.
- New Objective-C files or CocoaPods.
- Produce the minimal diff that accomplishes the task. Do not reformat untouched code.
- No explanatory comments in generated code, per the Comments Policy above.
- When multiple implementations are possible, choose the one with the lowest allocation count and peak memory, then the fewest lines, in that order.
- State any tradeoff you made (e.g., readability sacrificed for a hot-path optimization) in the summary, not in code comments.
- Confirm in the summary that the Memory Leak Detection checklist was run for any lifetime-touching change, and list any findings.