Skip to content

Latest commit

 

History

History
176 lines (137 loc) · 15.5 KB

File metadata and controls

176 lines (137 loc) · 15.5 KB

AGENTS.md

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.

Project Overview

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 UIViewRepresentable bridges where needed, e.g. WebKit).
  • Build: Xcode project at ios/LocalyticsDemoApp.xcodeproj; scheme LocalyticsDemoApp.
  • 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.

Language Rules

  • All new code MUST be Swift.
  • Use Swift idioms: value types (struct, enum) by default, protocol conformance over inheritance, guard for early exit, exhaustive switch, Result or typed throws for fallible paths.
  • Keep any @objc surface 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.

Comments Policy (STRICT)

  • 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.

Performance and Memory (HIGHEST PRIORITY)

Write the most memory-efficient, allocation-conscious code possible. Every allocation on a hot path is a defect.

Allocation discipline

  • Avoid heap allocation in hot paths: draw(_:), layoutSubviews, cellForRowAt/cell configuration, scroll callbacks, CADisplayLink ticks, audio/video render callbacks, and any per-frame code. Preallocate and reuse formatters, paths, buffers, and contexts as stored properties.
  • Prefer struct over class for models; keep structs small and avoid unintended copies of large ones (pass inout or 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 ContiguousArray for hot numeric arrays; use UnsafeBufferPointer/withUnsafeBytes only where profiling justifies it and the scope is contained.
  • Avoid boxing: no Any, AnyObject, or AnyHashable in hot paths; avoid protocol existentials where generics (some/<T>) allow static dispatch.
  • Mark classes final unless subclassed; use private/fileprivate to enable devirtualization and dead-code stripping.
  • DateFormatter, NumberFormatter, ISO8601DateFormatter, NSRegularExpression, and JSONDecoder are expensive: create once, store, reuse. Never instantiate them per cell or per call.
  • Never concatenate strings with + in loops; use a single String with reserveCapacity or joined().
  • Prefer lazy var for expensive main-thread-only properties; avoid lazy on structs.
  • Avoid KVO and NotificationCenter on hot paths; prefer direct delegation or Combine/async streams with bounded demand.

Memory leaks and ownership (ARC)

  • 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 var and 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] in sink.
  • Timers (Timer.scheduledTimer), CADisplayLink, and DispatchSourceTimer retain their targets: invalidate in deinit/teardown, or use the block-based API with [weak self].
  • NotificationCenter block-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.
  • URLSession with a delegate retains the delegate until finishTasksAndInvalidate()/invalidateAndCancel() is called; call it.

Images and resources

  • Downsample images to display size with ImageIO (CGImageSourceCreateThumbnailAtIndex with kCGImageSourceThumbnailMaxPixelSize) or UIGraphicsImageRenderer; never assign full-resolution UIImage(data:) to a thumbnail.
  • Use UIImage(named:) only for bundled assets that benefit from the system cache; use UIImage(contentsOfFile:) for large one-off images to avoid cache bloat.
  • Cache decoded images in NSCache with countLimit/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.

Concurrency

  • Use Swift Concurrency (async/await, actors) for new code; annotate UI-touching code @MainActor. No DispatchQueue.main.async sprinkled 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, pass NSManagedObjectID. Use fetchBatchSize and faulting; never fetch all rows to count them.

UI

  • This app is primarily SwiftUI. Keep body cheap and allocation-light; extract subviews; use Equatable views or stable identity to limit diffing; use @State/@StateObject correctly (@StateObject for owned reference models, @ObservedObject for injected); prefer LazyVStack/List for long content; avoid AnyView and .id(UUID()).
  • Deployment target is iOS 16: use ObservableObject (not the @Observable macro, which requires iOS 17+). If the deployment target is raised later, prefer @Observable where allowed.
  • When UIKit interop is required (UIViewRepresentable / UIViewControllerRepresentable): cell reuse is mandatory; no allocation or synchronous image decode in cellForRowAt; flatten view hierarchies; set shadowPath when using shadows; avoid offscreen rendering on scrolling content.
  • CALayer: set shouldRasterize only with measurement; avoid masks/shadows without shadowPath and corner radius + masksToBounds on scrolling content.

General

  • Prefer static dispatch: final, private, generics over existentials, structs over classes.
  • Avoid NSObject inheritance and dynamic dispatch in Swift unless required for interop (e.g. CLLocationManagerDelegate).
  • Avoid reflection (Mirror) and string-based selectors outside interop boundaries.
  • Prefer Codable with compile-time synthesis; no runtime schema reflection libraries on hot paths.

Architecture

  • 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).

Code Style

Never Nester Philosophy (MANDATORY)

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:

  1. 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 no else branches.
  2. 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 else after a guard or early return; the remaining code IS the else.
  • Loop bodies that need a condition use continue at the top, not a wrapping if.
  • Nested closures count as nesting: a completion handler inside a completion handler inside an if is already over budget; flatten with async/await or extract.
  • do/catch and switch cases 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.

General style

  • Swift API Design Guidelines. 4-space indent, no tabs.
  • Naming: descriptive and complete. remainingRetryCount, not cnt. 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!), or try! outside tests. Use guard let/if let with a handled failure path.
  • Visibility: everything private unless it must be wider.

Build and Verification

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 @available sprinkling 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.

Memory Leak Detection (REQUIRED)

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.

Tooling

  • 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 a deinit assertion 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.

Manual review checklist

Perform this static review on every diff before finishing:

  1. Every escaping or stored closure capturing self uses [weak self] (or a justified intentional strong capture stated in the summary).
  2. Every delegate/back-reference property is weak; no strong parent-child cycles in the object graph.
  3. Every Timer, CADisplayLink, and DispatchSourceTimer is invalidated/cancelled in teardown or uses a weak target pattern.
  4. Every NotificationCenter block observer token, KVO observation, and Combine AnyCancellable is stored and released with its owner.
  5. Every unstructured Task that could outlive its owner is stored and cancelled in teardown, or uses [weak self] and tolerates owner deallocation.
  6. URLSession delegate-based sessions are invalidated; long-lived sessions do not retain short-lived owners.
  7. No UIViewController, UIView, or view model reachable from a static property, global, or singleton beyond its lifetime.
  8. Animations and UIViewPropertyAnimators referencing views are stopped before their host is deallocated; CAAnimation delegates do not retain the layer's owner (they retain their delegate strongly).
  9. Core Data managed objects are not retained past their context's lifetime; background contexts are not captured strongly by long-lived closures.

Reporting

  • 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.

Prohibited

  • Force unwraps, force casts, try! outside tests; fatalError for recoverable conditions.
  • DispatchQueue.global().sync, semaphores to fake synchronous APIs, sleep/usleep on main.
  • unowned unless the lifetime relationship is structurally guaranteed and stated in the summary; default to weak.
  • Unbounded in-memory caches (Dictionary/NSMutableDictionary as cache); use NSCache with limits.
  • print/NSLog in release code paths; use the project's os.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.

Output Expectations for Agents

  • 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.