mobile-ai-agents 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +228 -0
- package/CLAUDE.md +236 -0
- package/LICENSE +21 -0
- package/README.md +652 -0
- package/agents/android/android-crash-analyzer/README.md +134 -0
- package/agents/android/android-crash-analyzer/agent.md +206 -0
- package/agents/android/android-crash-analyzer/example-input.md +138 -0
- package/agents/android/android-crash-analyzer/example-output.md +225 -0
- package/agents/android/anr-investigation/README.md +78 -0
- package/agents/android/anr-investigation/agent.md +117 -0
- package/agents/android/anr-investigation/example-input.md +62 -0
- package/agents/android/anr-investigation/example-output.md +103 -0
- package/agents/android/axiom/agent.md +197 -0
- package/agents/android/code-reviewer/README.md +108 -0
- package/agents/android/code-reviewer/agent.md +210 -0
- package/agents/android/code-reviewer/example-input.md +116 -0
- package/agents/android/code-reviewer/example-output.md +115 -0
- package/agents/android/compose-navigation/README.md +76 -0
- package/agents/android/compose-navigation/agent.md +121 -0
- package/agents/android/compose-navigation/example-input.md +49 -0
- package/agents/android/compose-navigation/example-output.md +325 -0
- package/agents/android/compose-screen-builder/README.md +95 -0
- package/agents/android/compose-screen-builder/agent.md +170 -0
- package/agents/android/compose-screen-builder/example-input.md +40 -0
- package/agents/android/compose-screen-builder/example-output.md +568 -0
- package/agents/android/compose-ui-reviewer/README.md +73 -0
- package/agents/android/compose-ui-reviewer/agent.md +253 -0
- package/agents/android/compose-ui-reviewer/example-input.md +84 -0
- package/agents/android/compose-ui-reviewer/example-output.md +102 -0
- package/agents/android/crash-analyzer/README.md +53 -0
- package/agents/android/crash-analyzer/agent.md +295 -0
- package/agents/android/memory-leak-analyzer/README.md +79 -0
- package/agents/android/memory-leak-analyzer/agent.md +116 -0
- package/agents/android/memory-leak-analyzer/example-input.md +78 -0
- package/agents/android/memory-leak-analyzer/example-output.md +128 -0
- package/agents/cross-platform/accessibility-auditor/README.md +47 -0
- package/agents/cross-platform/accessibility-auditor/agent.md +268 -0
- package/agents/cross-platform/accessibility-auditor/example-input.md +84 -0
- package/agents/cross-platform/accessibility-auditor/example-output.md +132 -0
- package/agents/cross-platform/appforge/agent.md +303 -0
- package/agents/cross-platform/ci-cd-generator/README.md +65 -0
- package/agents/cross-platform/ci-cd-generator/agent.md +422 -0
- package/agents/cross-platform/ci-cd-generator/example-input.md +69 -0
- package/agents/cross-platform/ci-cd-generator/example-output.md +261 -0
- package/agents/cross-platform/crasher/agent.md +221 -0
- package/agents/cross-platform/figma/agent.md +247 -0
- package/agents/cross-platform/launchpad/agent.md +330 -0
- package/agents/cross-platform/mobile-harness/agent.md +422 -0
- package/agents/cross-platform/mobile-memory/agent.md +429 -0
- package/agents/cross-platform/perf/agent.md +261 -0
- package/agents/cross-platform/pipeline/agent.md +288 -0
- package/agents/cross-platform/release-notes-generator/README.md +63 -0
- package/agents/cross-platform/release-notes-generator/agent.md +218 -0
- package/agents/cross-platform/release-notes-generator/example-input.md +71 -0
- package/agents/cross-platform/release-notes-generator/example-output.md +96 -0
- package/agents/cross-platform/scribe/agent.md +180 -0
- package/agents/cross-platform/security-scanner/README.md +61 -0
- package/agents/cross-platform/security-scanner/agent.md +287 -0
- package/agents/cross-platform/security-scanner/example-input.md +77 -0
- package/agents/cross-platform/security-scanner/example-output.md +141 -0
- package/agents/cross-platform/sentinel/agent.md +217 -0
- package/agents/cross-platform/store-listing-writer/README.md +58 -0
- package/agents/cross-platform/store-listing-writer/agent.md +380 -0
- package/agents/cross-platform/store-listing-writer/example-input.md +67 -0
- package/agents/cross-platform/store-listing-writer/example-output.md +131 -0
- package/agents/flutter/bloc-feature-builder/README.md +101 -0
- package/agents/flutter/bloc-feature-builder/agent.md +225 -0
- package/agents/flutter/bloc-feature-builder/example-input.md +47 -0
- package/agents/flutter/bloc-feature-builder/example-output.md +838 -0
- package/agents/flutter/dart/agent.md +223 -0
- package/agents/flutter/widget-generator/README.md +66 -0
- package/agents/flutter/widget-generator/agent.md +264 -0
- package/agents/flutter/widget-generator/example-input.md +71 -0
- package/agents/flutter/widget-generator/example-output.md +163 -0
- package/agents/gaming/forge/agent.md +197 -0
- package/agents/gaming/unreal/agent.md +200 -0
- package/agents/ios/crash-analyzer/README.md +69 -0
- package/agents/ios/crash-analyzer/agent.md +300 -0
- package/agents/ios/crash-analyzer/example-input.md +83 -0
- package/agents/ios/crash-analyzer/example-output.md +126 -0
- package/agents/ios/swift/agent.md +206 -0
- package/agents/ios/swift-reviewer/README.md +68 -0
- package/agents/ios/swift-reviewer/agent.md +231 -0
- package/agents/ios/swift-reviewer/example-input.md +100 -0
- package/agents/ios/swift-reviewer/example-output.md +127 -0
- package/agents/react-native/bridge/agent.md +212 -0
- package/agents/react-native/performance-optimizer/README.md +81 -0
- package/agents/react-native/performance-optimizer/agent.md +284 -0
- package/agents/react-native/performance-optimizer/example-input.md +104 -0
- package/agents/react-native/performance-optimizer/example-output.md +132 -0
- package/agents/unity/shader-generator/README.md +77 -0
- package/agents/unity/shader-generator/agent.md +270 -0
- package/agents/unity/shader-generator/example-input.md +70 -0
- package/agents/unity/shader-generator/example-output.md +160 -0
- package/agents/unreal/blueprint-advisor/README.md +71 -0
- package/agents/unreal/blueprint-advisor/agent.md +311 -0
- package/agents/unreal/blueprint-advisor/example-input.md +73 -0
- package/agents/unreal/blueprint-advisor/example-output.md +217 -0
- package/cli/index.js +1091 -0
- package/docs/agent-guide.md +138 -0
- package/docs/chatgpt-integration.md +219 -0
- package/docs/core-loops.md +143 -0
- package/docs/cursor-integration.md +143 -0
- package/docs/getting-started.md +132 -0
- package/docs/index.html +711 -0
- package/docs/installation.md +271 -0
- package/docs/mobile-harness.md +160 -0
- package/docs/mobile-mcp.md +172 -0
- package/docs/mobile-memory.md +349 -0
- package/docs/release-process.md +85 -0
- package/docs/releases/release-v1.0.0.md +165 -0
- package/docs/roadmap/github-issues.md +239 -0
- package/docs/skill-categories.md +125 -0
- package/docs/vscode-copilot-integration.md +104 -0
- package/docs/wiki.md +615 -0
- package/install.sh +507 -0
- package/mobile-karpathy.md +79 -0
- package/package.json +57 -0
- package/skills/android/README.md +31 -0
- package/skills/android/android-tdd.md +89 -0
- package/skills/android/anr-investigation.md +251 -0
- package/skills/android/code-review.md +98 -0
- package/skills/android/compose-migration.md +48 -0
- package/skills/android/compose-review.md +78 -0
- package/skills/android/kotlin-modernize.md +51 -0
- package/skills/android/memory-leak-investigation.md +256 -0
- package/skills/android/proguard-rules.md +48 -0
- package/skills/cross-platform/accessibility-audit.md +63 -0
- package/skills/cross-platform/api-versioning.md +59 -0
- package/skills/cross-platform/clean-code-audit.md +239 -0
- package/skills/cross-platform/crash-triage.md +53 -0
- package/skills/cross-platform/deeplink-debug.md +57 -0
- package/skills/cross-platform/feature-slice.md +48 -0
- package/skills/cross-platform/grill-mobile.md +64 -0
- package/skills/cross-platform/mobile-mcp-qa.md +193 -0
- package/skills/cross-platform/mobile-memory-graph.md +179 -0
- package/skills/cross-platform/mobile-memory-save.md +210 -0
- package/skills/cross-platform/mobile-memory-search.md +154 -0
- package/skills/cross-platform/perf-audit.md +61 -0
- package/skills/cross-platform/prd-verification.md +324 -0
- package/skills/cross-platform/release-prep.md +64 -0
- package/skills/cross-platform/security-audit.md +281 -0
- package/skills/cross-platform/store-listing.md +75 -0
- package/skills/flutter/README.md +32 -0
- package/skills/flutter/dart-modernize.md +47 -0
- package/skills/flutter/flutter-review.md +59 -0
- package/skills/flutter/flutter-tdd.md +73 -0
- package/skills/flutter/widget-extract.md +41 -0
- package/skills/flutter/widget-gen.md +131 -0
- package/skills/gaming/blueprint-to-cpp.md +51 -0
- package/skills/gaming/game-perf.md +54 -0
- package/skills/gaming/shader-gen.md +44 -0
- package/skills/gaming/unity-tdd.md +85 -0
- package/skills/ios/README.md +45 -0
- package/skills/ios/data-persistence.md +151 -0
- package/skills/ios/ios-tdd.md +90 -0
- package/skills/ios/networking.md +139 -0
- package/skills/ios/performance.md +122 -0
- package/skills/ios/swift-concurrency.md +57 -0
- package/skills/ios/swift-review.md +124 -0
- package/skills/ios/swiftui-review.md +49 -0
- package/skills/ios/swiftui-state.md +122 -0
- package/skills/ios/unit-testing.md +144 -0
- package/skills/ios/xcode-warnings.md +37 -0
- package/skills/react-native/README.md +42 -0
- package/skills/react-native/bridge-audit.md +104 -0
- package/skills/react-native/expo-optimize.md +55 -0
- package/skills/react-native/new-arch-migrate.md +53 -0
- package/skills/react-native/performance.md +124 -0
- package/skills/react-native/rn-review.md +53 -0
- package/skills/react-native/rn-tdd.md +76 -0
- package/skills/shared/README.md +46 -0
- package/skills/shared/accessibility-audit.md +132 -0
- package/skills/shared/crash-analysis.md +110 -0
- package/skills/shared/security-scan.md +149 -0
- package/skills/unity/README.md +31 -0
- package/skills/unity/shader-review.md +110 -0
- package/templates/agent-template.md +88 -0
- package/templates/prompt-template.md +50 -0
- package/templates/skill-template.md +64 -0
- package/templates/workflow-template.md +48 -0
- package/workflows/app-launch.md +66 -0
- package/workflows/appforge-workflow.md +232 -0
- package/workflows/ci-setup.md +77 -0
- package/workflows/crash-to-fix.md +71 -0
- package/workflows/feature-ship.md +61 -0
- package/workflows/game-level.md +72 -0
- package/workflows/mobile-harness.md +319 -0
- package/workflows/mobile-mcp-qa.md +228 -0
- package/workflows/mobile-memory-workflow.md +243 -0
- package/workflows/new-project-setup.md +93 -0
- package/workflows/new-screen.md +68 -0
- package/workflows/perf-sprint.md +78 -0
- package/workflows/release-cycle.md +71 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Skill — /unity-tdd
|
|
2
|
+
|
|
3
|
+
**Platform:** Unity (C#)
|
|
4
|
+
**Slash Command:** `/unity-tdd`
|
|
5
|
+
**Composable With:** agents/gaming/forge/agent.md
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
NUnit + Unity Test Runner TDD loop covering both EditMode (pure logic) and PlayMode (runtime behavior) tests. Generates failing tests first, then the minimal implementation.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Skill Prompt
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Run the Unity TDD loop using NUnit and Unity Test Runner:
|
|
19
|
+
|
|
20
|
+
STEP 1 — RED: Write the failing test.
|
|
21
|
+
- EditMode test: for pure C# logic (no MonoBehaviour, no scene needed).
|
|
22
|
+
[TestFixture] class, [Test] methods, [SetUp]/[TearDown].
|
|
23
|
+
- PlayMode test: for runtime behavior requiring scene, physics, or coroutines.
|
|
24
|
+
Use UnityTest with IEnumerator return, yield return null to advance frame.
|
|
25
|
+
- Use [UnitySetUp] / [UnityTearDown] for async setup in PlayMode.
|
|
26
|
+
|
|
27
|
+
STEP 2 — GREEN: Minimal implementation to pass.
|
|
28
|
+
STEP 3 — REFACTOR: Separate concerns, extract interfaces for testability.
|
|
29
|
+
|
|
30
|
+
TESTABILITY PATTERNS FOR UNITY:
|
|
31
|
+
- Avoid MonoBehaviour in business logic: extract to plain C# classes.
|
|
32
|
+
- Use interfaces for dependencies (IEnemyAI, IHealthSystem) to enable mocking.
|
|
33
|
+
- ScriptableObjects for data: easy to instantiate in tests without scene.
|
|
34
|
+
- Time.deltaTime dependency: inject a clock abstraction (IClock.DeltaTime).
|
|
35
|
+
|
|
36
|
+
Format:
|
|
37
|
+
--- RED ---
|
|
38
|
+
<NUnit test code>
|
|
39
|
+
|
|
40
|
+
--- GREEN ---
|
|
41
|
+
<minimal C# implementation>
|
|
42
|
+
|
|
43
|
+
--- REFACTOR ---
|
|
44
|
+
<final implementation>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Example
|
|
50
|
+
|
|
51
|
+
**Input:** "TDD a HealthSystem that reduces health on damage and fires OnDeath when reaching 0"
|
|
52
|
+
|
|
53
|
+
**Output:**
|
|
54
|
+
```csharp
|
|
55
|
+
--- RED ---
|
|
56
|
+
[TestFixture]
|
|
57
|
+
public class HealthSystemTests {
|
|
58
|
+
[Test]
|
|
59
|
+
public void TakeDamage_ReducesHealth() {
|
|
60
|
+
var health = new HealthSystem(maxHealth: 100);
|
|
61
|
+
health.TakeDamage(30);
|
|
62
|
+
Assert.AreEqual(70, health.Current);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[Test]
|
|
66
|
+
public void TakeDamage_ToZero_FiresOnDeath() {
|
|
67
|
+
var health = new HealthSystem(maxHealth: 100);
|
|
68
|
+
bool died = false;
|
|
69
|
+
health.OnDeath += () => died = true;
|
|
70
|
+
health.TakeDamage(100);
|
|
71
|
+
Assert.IsTrue(died);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
--- GREEN ---
|
|
76
|
+
public class HealthSystem {
|
|
77
|
+
public int Current { get; private set; }
|
|
78
|
+
public event Action OnDeath;
|
|
79
|
+
public HealthSystem(int maxHealth) => Current = maxHealth;
|
|
80
|
+
public void TakeDamage(int amount) {
|
|
81
|
+
Current = Mathf.Max(0, Current - amount);
|
|
82
|
+
if (Current == 0) OnDeath?.Invoke();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# iOS Skills
|
|
2
|
+
|
|
3
|
+
Reusable skill prompt modules for iOS / macOS (Swift / SwiftUI) code reviews and analysis. The most comprehensive platform coverage in the repo — 6 skills covering memory, concurrency, networking, testing, performance, and persistence.
|
|
4
|
+
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
| Skill | File | When to Use |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Swift Code Review | [swift-review.md](swift-review.md) | ARC retain cycles, `[weak self]`, `[unowned self]`, `@MainActor`, force-unwrap |
|
|
10
|
+
| SwiftUI State Management | [swiftui-state.md](swiftui-state.md) | `@State`/`@StateObject`/`@ObservedObject` selection, expensive `body`, lifecycle |
|
|
11
|
+
| iOS Networking | [networking.md](networking.md) | URLSession, async/await, HTTP status checks, certificate pinning, threading |
|
|
12
|
+
| iOS Unit Testing | [unit-testing.md](unit-testing.md) | Swift Testing vs XCTest, async tests, mocking with protocols |
|
|
13
|
+
| iOS Performance | [performance.md](performance.md) | Main thread blocking, Core Data N+1, image decoding, background task expiration |
|
|
14
|
+
| iOS Data Persistence | [data-persistence.md](data-persistence.md) | UserDefaults vs Keychain vs Core Data vs SwiftData, file encryption |
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Paste a skill prompt at the start of any LLM session:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
# Focused review
|
|
22
|
+
[paste skill prompt from swift-review.md]
|
|
23
|
+
|
|
24
|
+
Now review this Swift class: [paste code]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Composable Pattern
|
|
28
|
+
|
|
29
|
+
Combine skills for a comprehensive review:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
[paste swift-review.md skill]
|
|
33
|
+
[paste swiftui-state.md skill]
|
|
34
|
+
|
|
35
|
+
Review this SwiftUI ViewModel: [paste code]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Composable With
|
|
39
|
+
|
|
40
|
+
- [`agents/ios/swift-reviewer`](../../agents/ios/swift-reviewer/) — full structured review
|
|
41
|
+
- [`agents/ios/crash-analyzer`](../../agents/ios/crash-analyzer/) — crash debugging
|
|
42
|
+
|
|
43
|
+
## Contributing
|
|
44
|
+
|
|
45
|
+
New skills for iOS should cover one concern. Ideas: `combine-patterns.md`, `observable-macro.md` (iOS 17+), `swift-testing-advanced.md`. Copy `templates/skill-template.md`.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Skill — iOS Data Persistence
|
|
2
|
+
|
|
3
|
+
**Platform:** iOS / macOS (Swift)
|
|
4
|
+
**Category:** Code Quality
|
|
5
|
+
**Composable With:** agents/ios/swift-reviewer/agent.md, skills/shared/security-scan.md
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Reviews iOS data persistence code across Core Data, SwiftData, Keychain, and UserDefaults. Covers security, thread safety, migration strategy, and common pitfalls for each storage layer.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- When reviewing any code that reads from or writes to local storage
|
|
16
|
+
- When adding a new persistence layer to an existing app
|
|
17
|
+
- As a pre-release check for apps handling sensitive user data
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Skill Prompt
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
When reviewing iOS data persistence code, enforce these rules by storage layer:
|
|
25
|
+
|
|
26
|
+
USERDEFAULTS
|
|
27
|
+
- NEVER store: auth tokens, passwords, session IDs, credit card numbers, health data, or any PII.
|
|
28
|
+
UserDefaults is stored as a plaintext plist at /Library/Preferences — readable with device access.
|
|
29
|
+
Fix: use Keychain for any sensitive value.
|
|
30
|
+
- @AppStorage triggers a view body re-evaluation on every write. Flag @AppStorage for large or
|
|
31
|
+
frequently-written values — use a ViewModel with debounced persistence instead.
|
|
32
|
+
- Flag: UserDefaults.standard.synchronize() — deprecated, a no-op in modern iOS, and a sign
|
|
33
|
+
of cargo-cult code from older patterns.
|
|
34
|
+
- Appropriate uses: user preferences (theme, language), feature flag overrides, non-sensitive app state.
|
|
35
|
+
|
|
36
|
+
KEYCHAIN
|
|
37
|
+
- Use Security framework (SecItemAdd, SecItemCopyMatching) or a wrapper library (KeychainAccess).
|
|
38
|
+
- Set kSecAttrAccessible to kSecAttrAccessibleWhenUnlockedThisDeviceOnly for most cases.
|
|
39
|
+
Avoid kSecAttrAccessibleAlways — it persists across unlocks and is available in background.
|
|
40
|
+
- Flag: Keychain items shared across apps without explicit kSecAttrAccessGroup — unintended access.
|
|
41
|
+
- For biometric-protected items: use kSecAccessControlBiometryCurrentSet, not BiometryAny —
|
|
42
|
+
BiometryCurrentSet invalidates the item if a new fingerprint is enrolled (more secure).
|
|
43
|
+
- Always handle errSecItemNotFound gracefully — don't crash if a Keychain item doesn't exist yet.
|
|
44
|
+
|
|
45
|
+
CORE DATA
|
|
46
|
+
- Context threading: NEVER access a managed object from a different context's thread.
|
|
47
|
+
viewContext is main-thread only. For background work:
|
|
48
|
+
container.performBackgroundTask { backgroundContext in ... }
|
|
49
|
+
Or: NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
|
50
|
+
- Flag: NSManagedObject passed between threads without objectID re-fetch.
|
|
51
|
+
Fix: pass objectID across threads, then: let obj = backgroundContext.object(with: objectID)
|
|
52
|
+
- Flag: fetchRequest without a sortDescriptor — results are non-deterministic across saves.
|
|
53
|
+
- Flag: fetchRequest with no predicate fetching a large table — use predicates and fetchLimit.
|
|
54
|
+
- Migration: always add a NSMersistentStoreDescription with shouldMigrateStoreAutomatically = true
|
|
55
|
+
and a lightweight migration model when adding/removing attributes.
|
|
56
|
+
- Cascade delete rules: set delete rules on relationships to prevent orphaned objects.
|
|
57
|
+
|
|
58
|
+
SWIFTDATA (iOS 17+)
|
|
59
|
+
- @Model classes must be used with a ModelContainer — never create @Model instances outside a context.
|
|
60
|
+
- Queries (@Query) run on the main actor by default. For background inserts, use ModelActor:
|
|
61
|
+
@ModelActor actor DataImporter { func insert(_ items: [Item]) throws { ... } }
|
|
62
|
+
- Flag: @Model classes with large Data properties (images, files) — store file URLs, not raw data.
|
|
63
|
+
- Migration: use VersionedSchema and SchemaMigrationPlan for structural changes.
|
|
64
|
+
|
|
65
|
+
FILES
|
|
66
|
+
- Sensitive files must be stored in the Application Support directory with NSFileProtectionComplete:
|
|
67
|
+
try data.write(to: url, options: .completeFileProtection)
|
|
68
|
+
This encrypts the file when the device is locked.
|
|
69
|
+
- Flag: files written to the Documents directory without file protection — accessible via iTunes
|
|
70
|
+
sharing and potentially unencrypted on non-passcode devices.
|
|
71
|
+
- Flag: large files written synchronously on the main thread.
|
|
72
|
+
Fix: use a background Task or DispatchQueue.global(qos: .utility).
|
|
73
|
+
|
|
74
|
+
For each issue: state the storage layer, file/line, what is wrong, the security or correctness impact, and the corrected Swift snippet.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Example
|
|
80
|
+
|
|
81
|
+
**Input:**
|
|
82
|
+
```swift
|
|
83
|
+
func saveToken(_ token: String) {
|
|
84
|
+
UserDefaults.standard.set(token, forKey: "authToken")
|
|
85
|
+
UserDefaults.standard.synchronize()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func loadToken() -> String? {
|
|
89
|
+
UserDefaults.standard.string(forKey: "authToken")
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Output:**
|
|
94
|
+
```
|
|
95
|
+
CRITICAL — Auth token stored in UserDefaults (plaintext)
|
|
96
|
+
Impact: The token is stored at ~/Library/Preferences/<BundleID>.plist.
|
|
97
|
+
Any app with device access, jailbreak, or iTunes backup can read it.
|
|
98
|
+
Fix:
|
|
99
|
+
import Security
|
|
100
|
+
|
|
101
|
+
enum KeychainKey { static let authToken = "com.example.authToken" }
|
|
102
|
+
|
|
103
|
+
func saveToken(_ token: String) throws {
|
|
104
|
+
let data = Data(token.utf8)
|
|
105
|
+
let query: [CFString: Any] = [
|
|
106
|
+
kSecClass: kSecClassGenericPassword,
|
|
107
|
+
kSecAttrAccount: KeychainKey.authToken,
|
|
108
|
+
kSecValueData: data,
|
|
109
|
+
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
|
110
|
+
]
|
|
111
|
+
SecItemDelete(query as CFDictionary) // delete existing before adding
|
|
112
|
+
let status = SecItemAdd(query as CFDictionary, nil)
|
|
113
|
+
guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func loadToken() throws -> String? {
|
|
117
|
+
let query: [CFString: Any] = [
|
|
118
|
+
kSecClass: kSecClassGenericPassword,
|
|
119
|
+
kSecAttrAccount: KeychainKey.authToken,
|
|
120
|
+
kSecReturnData: true,
|
|
121
|
+
kSecMatchLimit: kSecMatchLimitOne
|
|
122
|
+
]
|
|
123
|
+
var result: AnyObject?
|
|
124
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
125
|
+
guard status == errSecSuccess, let data = result as? Data else {
|
|
126
|
+
return status == errSecItemNotFound ? nil : { throw KeychainError.loadFailed(status) }()
|
|
127
|
+
}
|
|
128
|
+
return String(data: data, encoding: .utf8)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
WARNING — UserDefaults.synchronize() is a deprecated no-op
|
|
132
|
+
Remove it. UserDefaults persists automatically.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Composition Example
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Combine with skills/shared/security-scan.md for a full data security audit:
|
|
141
|
+
[paste data-persistence skill] + [paste security-scan skill] → paste your persistence layer.
|
|
142
|
+
|
|
143
|
+
Or compose with skills/ios/networking.md when reviewing token storage after login flows.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Notes
|
|
149
|
+
|
|
150
|
+
- SwiftData is the recommended persistence layer for new iOS 17+ apps. For existing Core Data apps, migration is optional but worthwhile for new features.
|
|
151
|
+
- KeychainAccess (open-source library) wraps the Security framework in a clean API — recommended for readability over raw SecItem calls.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Skill — /ios-tdd
|
|
2
|
+
|
|
3
|
+
**Platform:** iOS (Swift)
|
|
4
|
+
**Slash Command:** `/ios-tdd`
|
|
5
|
+
**Composable With:** agents/ios/swift/agent.md
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Runs the red-green-refactor TDD loop for iOS — XCTest unit tests for Swift logic and XCUITest for SwiftUI component interaction. Generates the failing test first, then the minimal implementation.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Skill Prompt
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Run the iOS TDD loop for the described feature or function:
|
|
19
|
+
|
|
20
|
+
STEP 1 — RED: Write a failing XCTest.
|
|
21
|
+
- For business logic: XCTestCase with setUp/tearDown, XCTAssert* assertions.
|
|
22
|
+
- For async code: use async/await test methods with `await` and XCTAssertEqual.
|
|
23
|
+
- For ViewModel: mock dependencies via protocol injection.
|
|
24
|
+
- For SwiftUI: use ViewInspector or XCUITest depending on complexity.
|
|
25
|
+
- Test must fail for the right reason (assertion failure, not compile error).
|
|
26
|
+
|
|
27
|
+
STEP 2 — GREEN: Write the minimal Swift implementation to make the test pass.
|
|
28
|
+
- No over-engineering. No features not tested yet.
|
|
29
|
+
|
|
30
|
+
STEP 3 — REFACTOR: Clean up. Extract protocols, add computed properties, improve naming.
|
|
31
|
+
|
|
32
|
+
Format:
|
|
33
|
+
--- RED ---
|
|
34
|
+
<XCTest code>
|
|
35
|
+
|
|
36
|
+
--- GREEN ---
|
|
37
|
+
<minimal Swift implementation>
|
|
38
|
+
|
|
39
|
+
--- REFACTOR ---
|
|
40
|
+
<final Swift implementation>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Example
|
|
46
|
+
|
|
47
|
+
**Input:** "TDD a PriceFormatter that formats Double as currency string '£12.50'"
|
|
48
|
+
|
|
49
|
+
**Output:**
|
|
50
|
+
```swift
|
|
51
|
+
--- RED ---
|
|
52
|
+
class PriceFormatterTests: XCTestCase {
|
|
53
|
+
var sut: PriceFormatter!
|
|
54
|
+
override func setUp() { sut = PriceFormatter(currencyCode: "GBP") }
|
|
55
|
+
|
|
56
|
+
func test_format_producesCorrectCurrencyString() {
|
|
57
|
+
XCTAssertEqual(sut.format(12.5), "£12.50")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func test_format_handlesZero() {
|
|
61
|
+
XCTAssertEqual(sut.format(0), "£0.00")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
--- GREEN ---
|
|
66
|
+
struct PriceFormatter {
|
|
67
|
+
let currencyCode: String
|
|
68
|
+
func format(_ amount: Double) -> String {
|
|
69
|
+
let formatter = NumberFormatter()
|
|
70
|
+
formatter.numberStyle = .currency
|
|
71
|
+
formatter.currencyCode = currencyCode
|
|
72
|
+
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
--- REFACTOR ---
|
|
77
|
+
struct PriceFormatter {
|
|
78
|
+
private let formatter: NumberFormatter
|
|
79
|
+
|
|
80
|
+
init(currencyCode: String) {
|
|
81
|
+
formatter = NumberFormatter()
|
|
82
|
+
formatter.numberStyle = .currency
|
|
83
|
+
formatter.currencyCode = currencyCode
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func format(_ amount: Double) -> String {
|
|
87
|
+
formatter.string(from: NSNumber(value: amount)) ?? "—"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Skill — iOS Networking
|
|
2
|
+
|
|
3
|
+
**Platform:** iOS / macOS (Swift)
|
|
4
|
+
**Category:** Code Quality
|
|
5
|
+
**Composable With:** agents/ios/swift-reviewer/agent.md, skills/shared/security-scan.md
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Reviews iOS networking code for correctness, safety, and idiomatic Swift patterns. Covers URLSession, async/await, error handling, response decoding, certificate pinning, and cancellation.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- Reviewing any class that makes HTTP requests
|
|
16
|
+
- When migrating Alamofire or Combine networking to async/await
|
|
17
|
+
- As a pre-code-review checklist for API client classes
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Skill Prompt
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
When reviewing iOS Swift networking code, enforce these rules:
|
|
25
|
+
|
|
26
|
+
ASYNC/AWAIT PATTERNS
|
|
27
|
+
- Prefer URLSession.shared.data(from:) async throws over dataTask completion handlers in new code.
|
|
28
|
+
- Flag completion handler callbacks that aren't marked @escaping when they outlive the call site.
|
|
29
|
+
- For cancellable requests, store a Task handle as a property and cancel in deinit or onDisappear:
|
|
30
|
+
private var fetchTask: Task<Void, Never>?
|
|
31
|
+
func load() { fetchTask?.cancel(); fetchTask = Task { try? await fetch() } }
|
|
32
|
+
- Do not use URLSession in a struct that can be copied — retain the Task handle in a class or actor.
|
|
33
|
+
|
|
34
|
+
ERROR HANDLING
|
|
35
|
+
- Network errors should be modelled as a typed enum, not raw Error or String:
|
|
36
|
+
enum NetworkError: Error { case invalidResponse(Int), decodingFailed(Error), noConnection }
|
|
37
|
+
- Flag catch { } with no body — silently swallowed network errors mask bugs.
|
|
38
|
+
- Flag `try?` on network calls — it silently discards all error information. Use `do/catch`.
|
|
39
|
+
- HTTP status codes must be checked: a 404 or 500 returns a URLResponse, not an error.
|
|
40
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
41
|
+
(200..<300).contains(httpResponse.statusCode) else { throw NetworkError.invalidResponse(code) }
|
|
42
|
+
|
|
43
|
+
DECODING
|
|
44
|
+
- Always decode on a background thread, not MainActor, then publish the result on Main:
|
|
45
|
+
let decoded = try JSONDecoder().decode(T.self, from: data) // runs on caller's context
|
|
46
|
+
await MainActor.run { self.result = decoded }
|
|
47
|
+
- Use codingKeys or a custom dateDecodingStrategy — never rely on default ISO8601 parsing
|
|
48
|
+
without explicitly setting it: decoder.dateDecodingStrategy = .iso8601
|
|
49
|
+
- Flag forced try! on JSONDecoder().decode() — server responses can always be malformed.
|
|
50
|
+
|
|
51
|
+
SECURITY
|
|
52
|
+
- Flag URLSession configurations with URLSessionConfiguration that disables TLS validation:
|
|
53
|
+
session.delegate implementing didReceive challenge without real certificate evaluation → MitM risk.
|
|
54
|
+
- Flag http:// (cleartext) base URLs — use https:// exclusively.
|
|
55
|
+
- For sensitive endpoints (auth, payment), certificate pinning must be implemented in
|
|
56
|
+
URLSession.urlSession(_:didReceive:completionHandler:).
|
|
57
|
+
|
|
58
|
+
THREADING
|
|
59
|
+
- URLSession completion handlers run on an arbitrary background queue by default.
|
|
60
|
+
Any UI update after a completion handler must be dispatched to the main queue.
|
|
61
|
+
With async/await + @MainActor, this is handled automatically — prefer it.
|
|
62
|
+
- Flag URLSession.shared used in extensions or when a custom configuration is needed
|
|
63
|
+
(timeouts, caching policy, background transfers) — create a dedicated session instead.
|
|
64
|
+
|
|
65
|
+
MEMORY
|
|
66
|
+
- URLSession holds a strong reference to its delegate. The delegate must NOT hold a strong
|
|
67
|
+
reference back to the session object — this creates a retain cycle.
|
|
68
|
+
Fix: make the delegate weak (if possible) or use URLSession(configuration:) without a delegate
|
|
69
|
+
and use async/await instead.
|
|
70
|
+
|
|
71
|
+
For each issue: state the file/line, what is wrong, the risk, and the corrected Swift snippet.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Example
|
|
77
|
+
|
|
78
|
+
**Input:**
|
|
79
|
+
```swift
|
|
80
|
+
func fetchUser(id: String) {
|
|
81
|
+
URLSession.shared.dataTask(with: URL(string: "http://api.example.com/users/\(id)")!) { data, _, _ in
|
|
82
|
+
if let data = data {
|
|
83
|
+
self.user = try! JSONDecoder().decode(User.self, from: data)
|
|
84
|
+
}
|
|
85
|
+
}.resume()
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Output:**
|
|
90
|
+
```
|
|
91
|
+
Line 2 — Cleartext HTTP URL
|
|
92
|
+
Risk: All traffic is intercepted in plaintext. Use https://.
|
|
93
|
+
|
|
94
|
+
Line 2 — Force unwrap on URL(string:)
|
|
95
|
+
Risk: If the URL string is ever malformed (bad id encoding), the app crashes.
|
|
96
|
+
Fix: guard let url = URL(string: "https://api.example.com/users/\(id)") else { return }
|
|
97
|
+
|
|
98
|
+
Line 3 — No HTTP status code check
|
|
99
|
+
Risk: A 404 or 500 response will appear as "success" — the body may not be a User.
|
|
100
|
+
|
|
101
|
+
Line 3 — Strong self capture in completion handler (retain cycle risk)
|
|
102
|
+
Risk: If self is deallocated before the network call completes, accessing self.user crashes (if unowned) or leaks (if strong).
|
|
103
|
+
Fix: [weak self] guard let self else { return }
|
|
104
|
+
|
|
105
|
+
Line 4 — try! on JSONDecoder
|
|
106
|
+
Risk: If the server returns an error body or schema changes, the app crashes.
|
|
107
|
+
Fix: do { self.user = try JSONDecoder().decode(User.self, from: data) } catch { /* handle */ }
|
|
108
|
+
|
|
109
|
+
Line 3 — UI mutation on background queue
|
|
110
|
+
Risk: self.user likely drives the UI. Mutating it off the main thread is a data race.
|
|
111
|
+
Fix: DispatchQueue.main.async { self.user = decoded }
|
|
112
|
+
|
|
113
|
+
Idiomatic async/await replacement:
|
|
114
|
+
@MainActor
|
|
115
|
+
func fetchUser(id: String) async throws {
|
|
116
|
+
guard let url = URL(string: "https://api.example.com/users/\(id)") else { return }
|
|
117
|
+
let (data, response) = try await URLSession.shared.data(from: url)
|
|
118
|
+
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
|
119
|
+
throw NetworkError.invalidResponse((response as? HTTPURLResponse)?.statusCode ?? 0)
|
|
120
|
+
}
|
|
121
|
+
user = try JSONDecoder().decode(User.self, from: data)
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Composition Example
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Combine with skills/shared/security-scan.md for a full networking security audit:
|
|
131
|
+
[paste networking skill] + [paste security-scan skill] → paste your API client class.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- Alamofire adds its own validation layer (`validate()`) — when reviewing Alamofire code, check that `.validate()` is called on every request chain.
|
|
139
|
+
- Background URLSession (for large uploads/downloads) requires a separate `URLSessionConfiguration.background(withIdentifier:)` — document this if used.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Skill — iOS Performance
|
|
2
|
+
|
|
3
|
+
**Platform:** iOS / macOS (Swift / SwiftUI / UIKit)
|
|
4
|
+
**Category:** Performance & Optimization
|
|
5
|
+
**Composable With:** agents/ios/swift-reviewer/agent.md, skills/ios/swiftui-state.md
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Reviews iOS code for common performance pitfalls: main thread blocking, expensive view body, Core Data N+1 queries, image loading overhead, and background task misuse. Use as a pre-profiling code review before reaching for Instruments.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- When a screen feels laggy or slow to appear
|
|
16
|
+
- Before profiling with Instruments (eliminate obvious issues first)
|
|
17
|
+
- When reviewing any code that touches Core Data, images, or the main thread
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Skill Prompt
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
When reviewing iOS Swift code for performance, check for:
|
|
25
|
+
|
|
26
|
+
MAIN THREAD VIOLATIONS
|
|
27
|
+
- Disk I/O (FileManager, UserDefaults, CoreData fetch without async) on the main thread blocks
|
|
28
|
+
the UI thread. Any operation that could take >1ms must be on a background actor or DispatchQueue.global().
|
|
29
|
+
- URLSession.shared.dataTask result handling that mutates UI without dispatching to main.
|
|
30
|
+
- JSONDecoder().decode() on large payloads on the main thread — move to a Task { } or background queue.
|
|
31
|
+
- Flag: try FileManager.default.contentsOfDirectory(...) called directly in viewDidLoad or body.
|
|
32
|
+
|
|
33
|
+
SWIFTUI VIEW BODY
|
|
34
|
+
- body is called frequently. It must be a pure, cheap computation.
|
|
35
|
+
- Flag: DateFormatter() or NumberFormatter() instantiated inside body — these are expensive to create.
|
|
36
|
+
Fix: store as @State private var formatter = ... or as a static property.
|
|
37
|
+
- Flag: sorting, filtering, or mapping large arrays inside body.
|
|
38
|
+
Fix: use .onChange(of:) to recompute and store in @State, or use a ViewModel computed property with caching.
|
|
39
|
+
- Flag: body reading from UserDefaults directly — triggers a body re-evaluation on every read
|
|
40
|
+
if wrapped in @AppStorage, but UserDefaults is also synchronous disk access.
|
|
41
|
+
|
|
42
|
+
CORE DATA / SWIFTDATA
|
|
43
|
+
- N+1 query: fetching a list of objects, then accessing a relationship on each object in a loop
|
|
44
|
+
without prefetching. Each relationship access fires a new fetch.
|
|
45
|
+
Fix: set fetchRequest.relationshipKeyPathsForPrefetching = ["items"] to batch-load relationships.
|
|
46
|
+
- Fetching on the main context (viewContext) — heavy fetches block the UI.
|
|
47
|
+
Fix: use NSManagedObjectContext.perform { } or fetch on a background context with
|
|
48
|
+
PersistenceController.shared.container.performBackgroundTask { context in ... }
|
|
49
|
+
- Saving on every change instead of batching — each save triggers a write to disk.
|
|
50
|
+
Fix: batch saves with a debounced save call or save on significant lifecycle events only.
|
|
51
|
+
|
|
52
|
+
IMAGE LOADING
|
|
53
|
+
- Loading UIImage(named:) for large images synchronously on the main thread blocks rendering.
|
|
54
|
+
Fix: use async image loading (AsyncImage in SwiftUI, or a library like Kingfisher/Nuke).
|
|
55
|
+
- Decoding full-resolution images for thumbnail display — decoding a 4K image to show in a 80×80 cell wastes memory and CPU.
|
|
56
|
+
Fix: use ImageIO to decode at target size:
|
|
57
|
+
let options: [CFString: Any] = [kCGImageSourceThumbnailMaxPixelSize: 80,
|
|
58
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true]
|
|
59
|
+
- Not reusing images — storing UIImage objects without a cache in a list causes repeated disk reads.
|
|
60
|
+
|
|
61
|
+
BACKGROUND TASKS
|
|
62
|
+
- BGTaskScheduler tasks that do work on the main thread inside the task handler.
|
|
63
|
+
Fix: dispatch all work to a background thread inside the handler; call setTaskCompleted() in ALL code paths.
|
|
64
|
+
- Long-running background tasks (>30 seconds) that don't call setTaskCompleted() fast enough — watchdog kills the app (0x8badf00d).
|
|
65
|
+
- Doing CoreLocation, HealthKit, or CloudKit fetches in a UIBackgroundTask without proper expiration handling.
|
|
66
|
+
|
|
67
|
+
COLLECTION VIEW / TABLE VIEW
|
|
68
|
+
- dequeueReusableCell not called — creating new cells on every cellForRow is O(n) allocation per scroll event.
|
|
69
|
+
- Synchronous image loading inside cellForRow/itemForIndexPath — each scroll event blocks the main thread.
|
|
70
|
+
Fix: load images asynchronously and cancel on cell reuse (store the task in prepareForReuse).
|
|
71
|
+
- Recalculating cell heights in heightForRowAt with expensive layout passes — cache heights.
|
|
72
|
+
|
|
73
|
+
For each issue: state the file/line, the performance impact (what it blocks or how often it runs), and the corrected code.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Example
|
|
79
|
+
|
|
80
|
+
**Input:**
|
|
81
|
+
```swift
|
|
82
|
+
struct PostListView: View {
|
|
83
|
+
let posts: [Post]
|
|
84
|
+
|
|
85
|
+
var body: some View {
|
|
86
|
+
let sorted = posts.sorted { $0.date > $1.date } // every rebuild
|
|
87
|
+
List(sorted) { post in
|
|
88
|
+
Text(DateFormatter().string(from: post.date)) // new formatter per cell
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Output:**
|
|
95
|
+
```
|
|
96
|
+
body — sorted array computed on every rebuild
|
|
97
|
+
Impact: .sorted is O(n log n). With 500 posts, this runs on every body call triggered
|
|
98
|
+
by any parent state change, causing visible lag during scroll.
|
|
99
|
+
Fix:
|
|
100
|
+
// In ViewModel or parent:
|
|
101
|
+
@State private var sortedPosts = posts.sorted { $0.date > $1.date }
|
|
102
|
+
// Update only when posts changes with .onChange(of: posts) { sortedPosts = ... }
|
|
103
|
+
|
|
104
|
+
body — DateFormatter instantiated per cell per rebuild
|
|
105
|
+
Impact: DateFormatter initialization is expensive (locale setup, calendar, timezone).
|
|
106
|
+
With 20 visible cells and frequent rebuilds, this creates hundreds of objects per second.
|
|
107
|
+
Fix:
|
|
108
|
+
private static let dateFormatter: DateFormatter = {
|
|
109
|
+
let f = DateFormatter()
|
|
110
|
+
f.dateStyle = .medium
|
|
111
|
+
return f
|
|
112
|
+
}()
|
|
113
|
+
// Or use .formatted() with a FormatStyle (no allocation):
|
|
114
|
+
Text(post.date.formatted(date: .abbreviated, time: .omitted))
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Notes
|
|
120
|
+
|
|
121
|
+
- Instruments > Time Profiler is the ground truth for main thread blocking — use this skill for pre-profiling review, not as a replacement for measurement.
|
|
122
|
+
- SwiftUI's view body performance has improved significantly in iOS 17 with the `@Observable` macro — less state change propagation overhead compared to `ObservableObject`.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Skill — /swift-concurrency
|
|
2
|
+
|
|
3
|
+
**Platform:** iOS (Swift)
|
|
4
|
+
**Slash Command:** `/swift-concurrency`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Safely converts completion handler callbacks to Swift async/await. Handles nested callbacks, error mapping, DispatchQueue patterns, and Combine pipelines.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Skill Prompt
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Convert the provided Swift completion handler code to async/await safely:
|
|
18
|
+
|
|
19
|
+
CONVERSION PATTERNS:
|
|
20
|
+
1. Simple callback → async function:
|
|
21
|
+
func fetchUser(id: String, completion: @escaping (User?, Error?) -> Void)
|
|
22
|
+
→ func fetchUser(id: String) async throws -> User
|
|
23
|
+
|
|
24
|
+
2. Result-based callback → async throws:
|
|
25
|
+
func loadData(completion: @escaping (Result<Data, Error>) -> Void)
|
|
26
|
+
→ func loadData() async throws -> Data
|
|
27
|
+
|
|
28
|
+
3. Nested callbacks → sequential await:
|
|
29
|
+
Flatten callback pyramids into sequential async calls.
|
|
30
|
+
|
|
31
|
+
4. DispatchQueue.main.async callback → @MainActor:
|
|
32
|
+
func updateUI() { DispatchQueue.main.async { self.label.text = "done" } }
|
|
33
|
+
→ @MainActor func updateUI() { label.text = "done" }
|
|
34
|
+
|
|
35
|
+
5. Withstand continuations for legacy APIs:
|
|
36
|
+
Use withCheckedThrowingContinuation for APIs you can't change:
|
|
37
|
+
func legacyFetch() async throws -> Data {
|
|
38
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
39
|
+
legacyAPI.fetch { data, error in
|
|
40
|
+
if let error { continuation.resume(throwing: error) }
|
|
41
|
+
else { continuation.resume(returning: data!) }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
6. Combine → AsyncStream / async sequence:
|
|
47
|
+
publisher.values async sequence in for await loops.
|
|
48
|
+
|
|
49
|
+
SAFETY CHECKLIST:
|
|
50
|
+
- [ ] continuation.resume called exactly once (not zero, not twice)
|
|
51
|
+
- [ ] No @escaping closures capturing self strongly after async conversion
|
|
52
|
+
- [ ] @MainActor added to functions that update UI
|
|
53
|
+
- [ ] Task { } used only when fire-and-forget is intentional; add error handling
|
|
54
|
+
|
|
55
|
+
Show before and after for each function converted. Flag any pattern that cannot be
|
|
56
|
+
safely converted without changing the calling contract.
|
|
57
|
+
```
|