ios-at-scale/SKILL.md
Production iOS engineering for large teams and apps — modular architecture (RIBLETS, ComponentKit), Buck/Bazel build systems, trunk-based development, CI/CD pipeline design with coverage-based test selection, feature flags as release...
npx skillsauth add peterbamuhigire/skills-web-dev ios-at-scaleInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
ios-at-scale or would be better handled by a more specific companion skill.SKILL.md first, then load only the referenced deep-dive files that are necessary for the task.Production-grade iOS engineering for large teams and apps. Distilled from Uber, Meta, Airbnb engineering decisions and iOS 18 fundamentals.
Correct Gang-of-Four patterns still fail at scale if everything lives in one framework. A monolith means:
Swift module boundaries are architectural enforcement: internal types cannot cross module boundaries. Designing modules correctly makes it structurally impossible for teams to accidentally couple domains.
After monolith collapse, Uber created RIBLETS: Router, Interactor, Builder, optional Leaf, Extension, Segue.
Each RIBLET is a self-contained module:
Unidirectional data flow — Interactor → Presenter → View — prevents state corruption across features. Service layer streams are immutable; Interactors cannot mutate shared state directly.
Modularising the UI rendering layer into its own framework let a dedicated team optimise independently.
Results:
Pattern: Large-scale UI optimisation requires isolating the rendering layer before optimising it. You cannot optimise what you cannot isolate.
| System | Caching | Parallel Builds | Scales To | |--------|---------|-----------------|-----------| | CocoaPods | None | No | Small teams | | SPM | Limited | No | Mid-size teams | | Buck/Bazel | Remote + distributed | Yes | 100+ engineers |
CocoaPods rebuilds full modules on any change. SPM improves dependency management but lacks build caching. At scale, both are too slow.
Airbnb result: flat directory structure caused 1-2 minute Xcode workspace load times. After adopting Buck, CI build times dropped 50%.
Explicitly recommended over long-running feature branches. At 100+ PRs/hour, long branches produce compounding merge conflicts that become their own engineering workstream.
The contract: every commit to main must be production-safe. Feature flags gate unreleased work — not branches.
| Stage | Trigger | Feedback Latency | Cost | |-------|---------|-----------------|------| | PR Submitted | Per PR | Earliest | Highest per-unit | | Pre-Merge | At merge time | Early | High | | Post-Commit | After landing on main | Delayed | High | | Continuous | Scheduled (nightly) | Latest | Low |
Running the full test suite on every PR is prohibitively expensive. Fast snapshot tests run on PR submission; broader integration tests run at merge; full suite runs on schedule. This is not a compromise — it is a deliberate cost/signal tradeoff.
Engineers rotate responsibility for monitoring CI failures and reverting offending commits. Required because pre-merge tests will occasionally miss real failures. Without this rotation, a broken main becomes normalised.
A flaky test must be fixed or deleted. No exceptions.
Engineers who bypass failing tests because "it's probably flaky" destroy the entire CI culture. The degradation is exponential:
Feature flags are the enabling infrastructure for trunk-based development and safe releases. Treat them as a first-class platform concern, not a per-feature convenience.
| Capability | Description | |------------|-------------| | Safe trunk landings | Land features on main behind a flag — main is always shippable | | Dogfooding | Internal employees → QA → limited external users | | Phased rollout | 1% → 10% → 50% → 100% — observe metrics at each threshold | | Kill switches | Disable any feature in production within minutes, no App Store submission | | A/B testing | Hold out a cohort to confirm metric gains causally |
After full rollout, hold out a small cohort (e.g. 2%) with the feature still disabled. Compare metrics. This confirms the feature caused the improvement rather than concurrent changes. Without this, you cannot distinguish causation from coincidence.
Feature flags plus phased rollout are the mobile answer to web's instant rollback. There is no other mechanism.
Tool-agnostic principles. Works with Xcode Cloud, GitHub Actions, Fastlane, Bitrise.
Only run tests for modules actually affected by the diff. A UI module change must not trigger the full networking module test suite.
Implementation: build a dependency graph (Buck/Bazel provides this natively). For each PR, compute the set of affected modules, run only their associated test targets.
If two tests always pass or fail together, they are testing the same code path. Run only one per diff. Over thousands of PRs per week, this meaningfully reduces CI cost without reducing coverage signal.
| Metric | Definition | Target | |--------|-----------|--------| | Reliability | % of PRs that pass CI without a false failure | >99% | | Correctness | How often main breaks after a green CI run | <0.5% | | Time-to-signal | Minutes engineers wait for feedback | <10 min |
A CI system where engineers routinely bypass failures is worse than no CI at all — it creates the illusion of safety without the substance.
Use Fastlane for automation regardless of primary CI system:
match for certificate/provisioning profile management)Integrate Fastlane lanes into Buck/Bazel build definitions so CI and local builds use identical pipelines.
Never evaluate performance using averages. Track P50, P90, P99.
Dismissing P99 as "only 1% of users" is a business mistake. That cohort may be your highest-value or most loyal segment. P99 regressions also predict future P90 regressions as usage grows.
Funnel logging: instrument time at each pipeline stage for every user-facing operation:
Network request sent → Response received → Data parsed → First frame rendered
When a percentile degrades, the funnel tells you which stage regressed.
Apple target: first frame in ≤400ms.
Profiling environment: oldest supported device, release build, after reboot, airplane mode with network mocked. Never profile on a developer machine with a debug build — results are not meaningful.
Startup optimisation checklist:
□ Move non-essential initialisation out of UIApplicationDelegate
□ Replace +load with +initialize (lazy — called only when class first used)
□ Remove all unused linked frameworks (each adds DYLD3 linker cost)
□ Hard-link all dependencies (DYLD3 gains full visibility for pre-linking)
□ Lazy-load views not visible at launch
□ Push all non-critical work to background queues during startup
□ Replace String(describing:) with ObjectIdentifier for type identification
String(describing:) performs a protocol conformance check at runtime. ObjectIdentifier uses the type's memory address — O(1), no conformance lookup.
DoorDash engineering data: replacing String(describing:) gave 11% faster startup; hash value redesign added a further 29%.
Profiling tools:
50 engineers each adding a "minor" feature with "no performance impact" produces severe regression over 2-3 years. Hardware improvements mask it until a new device generation stops arriving.
Performance lifecycle (non-negotiable from day one):
XCTestMetricactor UserSessionStore {
private var sessions: [String: UserSession] = [:]
func store(_ session: UserSession, for userID: String) {
sessions[userID] = session
}
func session(for userID: String) -> UserSession? {
sessions[userID]
}
}
Actor advantage over GCD serial queues: priority-based re-ordering. Swift actors avoid priority inversion — a high-priority task is not forced to wait behind a low-priority task queued earlier on the same serial queue.
Task.detached(priority: .background) {
await withTaskGroup(of: Void.self) { group in
group.addTask { await writeToCache(objects) }
group.addTask { await logResult() }
}
}
// Cancelling the top-level task automatically cancels all child tasks.
// Child tasks inherit parent priority — no manual priority propagation needed.
Structured concurrency makes the task tree explicit. Cancellation propagates downward automatically — no dangling async work after a parent completes.
await is an explicit suspension point. The thread running the async function may change after each await. Never hold a lock across an await — this violates the Swift runtime's forward progress contract and can deadlock.
// WRONG — lock held across suspension point
actor BrokenActor {
var lock = NSLock()
func dangerousMethod() async {
lock.lock()
await someAsyncWork() // thread may change here; lock held by wrong thread
lock.unlock()
}
}
// CORRECT — actors provide implicit mutual exclusion; no explicit lock needed
actor CorrectActor {
func safeMethod() async {
await someAsyncWork() // suspension is safe; actor serialises access
}
}
Architecture principle: encapsulate concurrency at the library boundary. Callers of your module's API should not need to manage concurrent access — that is the module's responsibility, not the caller's.
@Model class JournalEntry {
var date: Date
var title: String
@Attribute(.externalStorage) var photoData: Data?
// Stores binary in adjacent file, not inline in the model store.
// Without this, large blobs slow every fetch query for that model type.
}
SwiftData does not support UIImage directly — convert to Data for storage; decode asynchronously with Task. Inline blob storage is the single most common SwiftData performance mistake.
WWDC 2024 additions:
import Testing
@testable import MyApp
struct MyTests {
@Test func validInitializationSucceeds() {
let entry = JournalEntry(rating: 3, title: "Test")
#expect(entry != nil)
// #expect shows actual values in failures — not just "assertion failed"
// Dramatically reduces time spent diagnosing failures in CI
}
@Test("Rejects out-of-range ratings", arguments: [-1, 0, 6, 100])
func rejectsInvalidRating(rating: Int) {
#expect(JournalEntry(rating: rating, title: "x") == nil)
}
}
setUp/tearDown lifecycle required#expect macro provides rich diagnostics with actual and expected valuesarguments: eliminate boundary-condition boilerplateWriting Tools appear automatically in UITextView. Handle delegate callbacks to prevent data consistency issues during AI transformation:
func textViewWritingToolsWillBegin(_ textView: UITextView) {
textView.isEditable = false // prevent data corruption during AI transformation
}
func textViewWritingToolsDidEnd(_ textView: UITextView) {
textView.isEditable = true
}
// Opt out entirely for fields where AI transformation is inappropriate:
textView.writingToolsBehavior = .none
// e.g. code editors, password fields, structured data entry forms
@State private var navigationPath = NavigationPath()
NavigationStack(path: $navigationPath) {
ContentView()
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
// Type-safe routing: destination determined by value type, not string tag
}
.navigationDestination(for: SettingsRoute.self) { route in
SettingsView(route: route)
}
}
// Programmatic navigation — type-safe, decoupled from view hierarchy:
navigationPath.append(selectedItem)
// Deep link: replace entire stack programmatically
navigationPath = NavigationPath([rootItem, childItem])
NavigationStack replaces deprecated NavigationView. The path binding enables deep linking and programmatic stack manipulation — essential for feature-flag-controlled onboarding flows and push notification routing.
| Anti-Pattern | Why It Fails | Correct Approach |
|-------------|-------------|-----------------|
| Monolithic app target | Full recompile on any change; no team isolation | Modular framework graph with Buck/Bazel |
| Average-based performance metrics | Hides P99 cohort problems | Track P50/P90/P99 with funnel logging |
| Releases without feature flags | Any regression requires new App Store submission | Feature flags on everything; phased rollout |
| Long-running feature branches | Compound merge conflicts at 100+ PRs/day | Trunk-based development; flags gate dark code |
| Tolerating flaky tests | Engineers stop trusting CI; pipeline becomes theatre | Fix or delete; zero tolerance policy |
| +load methods | Eager class initialisation at startup adds unmeasured latency | Replace with +initialize (lazy) |
| String(describing:) for type identification | Protocol conformance check on every call | Use ObjectIdentifier (pointer equality, O(1)) |
| NSLock across await | Violates forward progress contract; potential deadlock | Use actors for mutual exclusion |
| Inline blob storage in SwiftData | Slows all model fetches | @Attribute(.externalStorage) for all Data |
| Manual certificate management in CI | Brittle, breaks on rotation | Fastlane match with shared certificate repo |
data-ai
Use when adding AI-powered analytics to a SaaS platform — semantic search over business data, natural language queries, trend detection, anomaly alerts, and AI-generated insights for dashboards. Covers embeddings, NL2SQL, and per-tenant analytics...
data-ai
Design AI-powered analytics dashboards — what metrics to show, how to display AI predictions and confidence, drill-down patterns, KPI cards, trend visualisation, AI Insights panels, export design, and role-based dashboard variants. Invoke when...
development
Use when designing, building, reviewing, or upgrading production software systems that must be secure, performant, maintainable, scalable, and user-centered. Apply before writing specs, code, architecture, APIs, databases, mobile apps, SaaS platforms, or ERP systems.
development
Professional web app UI using commercial templates (Tabler/Bootstrap 5) with strong frontend design direction when needed. Use for CRUD interfaces, dashboards, admin panels with SweetAlert2, DataTables, Flatpickr. Clone seeder-page.php, use...