Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-modernize/.openskills.json
Normal file
7
.claude/skills/axiom-modernize/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-modernize",
|
||||
"installedAt": "2026-04-12T08:06:29.425Z"
|
||||
}
|
||||
546
.claude/skills/axiom-modernize/SKILL.md
Normal file
546
.claude/skills/axiom-modernize/SKILL.md
Normal file
@@ -0,0 +1,546 @@
|
||||
---
|
||||
name: axiom-modernize
|
||||
description: Use when the user wants to modernize iOS code to iOS 17/18 patterns, migrate from ObservableObject to @Observable, update @StateObject to @State, or adopt modern SwiftUI APIs.
|
||||
license: MIT
|
||||
disable-model-invocation: true
|
||||
---
|
||||
# Modernization Helper Agent
|
||||
|
||||
You are an expert at migrating iOS apps to modern iOS 17/18+ patterns.
|
||||
|
||||
## Your Mission
|
||||
|
||||
Scan the codebase for legacy patterns and provide migration paths:
|
||||
- `ObservableObject` → `@Observable`
|
||||
- `@StateObject` → `@State` with Observable
|
||||
- `@ObservedObject` → Direct property or `@Bindable`
|
||||
- `@EnvironmentObject` → `@Environment`
|
||||
- Legacy SwiftUI modifiers → Modern equivalents
|
||||
- Completion handlers → async/await
|
||||
|
||||
Report findings with:
|
||||
- File:line references
|
||||
- Priority (HIGH/MEDIUM/LOW based on benefit)
|
||||
- Migration code examples
|
||||
- Breaking change warnings
|
||||
|
||||
## Files to Scan
|
||||
|
||||
**Swift files**: `**/*.swift`
|
||||
Skip: `*Tests.swift`, `*Previews.swift`, `*/Pods/*`, `*/Carthage/*`, `*/.build/*`, `*/DerivedData/*`, `*/scratch/*`, `*/docs/*`, `*/.claude/*`, `*/.claude-plugin/*`
|
||||
|
||||
## Modernization Patterns (iOS 17+ / iOS 18+)
|
||||
|
||||
### Pattern 1: ObservableObject → @Observable (HIGH)
|
||||
|
||||
**Why migrate**: Better performance (view updates only when accessed properties change), simpler syntax, no `@Published` needed
|
||||
|
||||
**Requirement**: iOS 17+
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: class.*ObservableObject
|
||||
Grep: : ObservableObject
|
||||
Grep: @Published
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ LEGACY (iOS 14-16)
|
||||
class ContentViewModel: ObservableObject {
|
||||
@Published var items: [Item] = []
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage: String?
|
||||
}
|
||||
|
||||
// ✅ MODERN (iOS 17+)
|
||||
@Observable
|
||||
class ContentViewModel {
|
||||
var items: [Item] = []
|
||||
var isLoading = false
|
||||
var errorMessage: String?
|
||||
|
||||
// Use @ObservationIgnored for non-observed properties
|
||||
@ObservationIgnored
|
||||
var internalCache: [String: Any] = [:]
|
||||
}
|
||||
```
|
||||
|
||||
**Migration steps**:
|
||||
1. Replace `: ObservableObject` with `@Observable` macro
|
||||
2. Remove all `@Published` property wrappers
|
||||
3. Add `@ObservationIgnored` to properties that shouldn't trigger updates
|
||||
4. Update consuming views (see patterns below)
|
||||
|
||||
### Pattern 2: @StateObject → @State (HIGH)
|
||||
|
||||
**Why migrate**: Simpler, consistent with value types, works with @Observable
|
||||
|
||||
**Requirement**: iOS 17+ with @Observable model
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: @StateObject
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ LEGACY
|
||||
struct ContentView: View {
|
||||
@StateObject private var viewModel = ContentViewModel()
|
||||
|
||||
var body: some View { ... }
|
||||
}
|
||||
|
||||
// ✅ MODERN (with @Observable model)
|
||||
struct ContentView: View {
|
||||
@State private var viewModel = ContentViewModel()
|
||||
|
||||
var body: some View { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Only migrate after the model uses `@Observable`. If model still uses `ObservableObject`, keep `@StateObject`.
|
||||
|
||||
### Pattern 3: @ObservedObject → Direct Property or @Bindable (HIGH)
|
||||
|
||||
**Why migrate**: Simpler code, explicit binding when needed
|
||||
|
||||
**Requirement**: iOS 17+ with @Observable model
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: @ObservedObject
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ LEGACY
|
||||
struct ItemView: View {
|
||||
@ObservedObject var item: ItemModel
|
||||
|
||||
var body: some View {
|
||||
Text(item.name)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ MODERN - Direct property (read-only access)
|
||||
struct ItemView: View {
|
||||
var item: ItemModel // No wrapper needed!
|
||||
|
||||
var body: some View {
|
||||
Text(item.name)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ MODERN - @Bindable (for two-way binding)
|
||||
struct ItemEditorView: View {
|
||||
@Bindable var item: ItemModel
|
||||
|
||||
var body: some View {
|
||||
TextField("Name", text: $item.name) // Binding works
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Decision tree**:
|
||||
- Need binding (`$item.property`)? → Use `@Bindable`
|
||||
- Just reading properties? → Use plain property (no wrapper)
|
||||
|
||||
### Pattern 4: @EnvironmentObject → @Environment (HIGH)
|
||||
|
||||
**Why migrate**: Type-safe, works with @Observable
|
||||
|
||||
**Requirement**: iOS 17+ with @Observable model
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: @EnvironmentObject
|
||||
Grep: \.environmentObject\(
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ LEGACY - Setting
|
||||
ContentView()
|
||||
.environmentObject(settings)
|
||||
|
||||
// ❌ LEGACY - Reading
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var settings: AppSettings
|
||||
|
||||
var body: some View { ... }
|
||||
}
|
||||
|
||||
// ✅ MODERN - Setting
|
||||
ContentView()
|
||||
.environment(settings)
|
||||
|
||||
// ✅ MODERN - Reading
|
||||
struct SettingsView: View {
|
||||
@Environment(AppSettings.self) var settings
|
||||
|
||||
var body: some View { ... }
|
||||
}
|
||||
|
||||
// ✅ MODERN - With binding
|
||||
struct SettingsEditorView: View {
|
||||
@Environment(AppSettings.self) var settings
|
||||
|
||||
var body: some View {
|
||||
@Bindable var settings = settings
|
||||
Toggle("Dark Mode", isOn: $settings.darkMode)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: onChange(of:perform:) → onChange(of:initial:_:) (MEDIUM)
|
||||
|
||||
**Why migrate**: Deprecated modifier, new API has `initial` parameter
|
||||
|
||||
**Requirement**: iOS 17+
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: \.onChange\(of:.*perform:
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ DEPRECATED
|
||||
.onChange(of: searchText) { newValue in
|
||||
performSearch(newValue)
|
||||
}
|
||||
|
||||
// ✅ MODERN (iOS 17+)
|
||||
.onChange(of: searchText) { oldValue, newValue in
|
||||
performSearch(newValue)
|
||||
}
|
||||
|
||||
// ✅ With initial execution
|
||||
.onChange(of: searchText, initial: true) { oldValue, newValue in
|
||||
performSearch(newValue)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 6: Completion Handlers → async/await (MEDIUM)
|
||||
|
||||
**Why migrate**: Cleaner code, better error handling, structured concurrency
|
||||
|
||||
**Requirement**: iOS 15+ (widely adopted in iOS 17+)
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: completion:\s*@escaping
|
||||
Grep: completionHandler:
|
||||
Grep: DispatchQueue\.main\.async
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ LEGACY
|
||||
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
|
||||
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
// Parse and return
|
||||
completion(.success(user))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// ✅ MODERN
|
||||
func fetchUser(id: String) async throws -> User {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
return try JSONDecoder().decode(User.self, from: data)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 7: withAnimation Closures → Animation Parameter (LOW)
|
||||
|
||||
**Why migrate**: Cleaner API, avoids closure
|
||||
|
||||
**Requirement**: iOS 17+
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: withAnimation.*\{
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ LEGACY
|
||||
withAnimation(.spring()) {
|
||||
isExpanded.toggle()
|
||||
}
|
||||
|
||||
// ✅ MODERN (simple cases)
|
||||
isExpanded.toggle()
|
||||
// Apply animation to view:
|
||||
.animation(.spring(), value: isExpanded)
|
||||
|
||||
// Or use new binding animation:
|
||||
$isExpanded.animation(.spring()).wrappedValue.toggle()
|
||||
```
|
||||
|
||||
### Pattern 8: Swift Language Modernization (LOW)
|
||||
|
||||
**Why migrate**: Clearer, more efficient, modern Swift idioms
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: Date\(\)
|
||||
Grep: CGFloat
|
||||
Grep: replacingOccurrences
|
||||
Grep: DateFormatter\(\)
|
||||
Grep: \.filter\(.*\)\.count
|
||||
Grep: Task\.sleep\(nanoseconds:
|
||||
```
|
||||
|
||||
**Reference**: See `axiom-swift-modern` skill for the full modern API replacement table.
|
||||
|
||||
Report matches as LOW priority unless they appear in hot paths (then MEDIUM).
|
||||
|
||||
## Audit Process
|
||||
|
||||
### Step 1: Find Swift Files
|
||||
|
||||
```
|
||||
Glob: **/*.swift
|
||||
```
|
||||
|
||||
### Step 2: Detect Legacy Patterns
|
||||
|
||||
**ObservableObject**:
|
||||
```
|
||||
Grep: ObservableObject
|
||||
Grep: @Published
|
||||
```
|
||||
|
||||
**Property Wrappers**:
|
||||
```
|
||||
Grep: @StateObject|@ObservedObject|@EnvironmentObject
|
||||
```
|
||||
|
||||
**Deprecated Modifiers**:
|
||||
```
|
||||
Grep: onChange\(of:.*perform:
|
||||
```
|
||||
|
||||
**Completion Handlers**:
|
||||
```
|
||||
Grep: completion:\s*@escaping
|
||||
Grep: completionHandler:
|
||||
```
|
||||
|
||||
### Step 3: Categorize by Priority
|
||||
|
||||
**HIGH Priority** (significant benefits):
|
||||
- ObservableObject → @Observable
|
||||
- Property wrapper migrations
|
||||
|
||||
**MEDIUM Priority** (code quality):
|
||||
- Deprecated modifiers
|
||||
- async/await adoption
|
||||
|
||||
**LOW Priority** (minor improvements):
|
||||
- Animation syntax
|
||||
- Minor API updates
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Modernization Analysis Results
|
||||
|
||||
## Summary
|
||||
- **HIGH Priority**: [count] (Significant performance/maintainability gains)
|
||||
- **MEDIUM Priority**: [count] (Deprecated APIs, code quality)
|
||||
- **LOW Priority**: [count] (Minor improvements)
|
||||
|
||||
## Minimum Deployment Target Impact
|
||||
- Current patterns support: iOS 14+
|
||||
- After full modernization: iOS 17+
|
||||
|
||||
## HIGH Priority Migrations
|
||||
|
||||
### ObservableObject → @Observable
|
||||
|
||||
**Files affected**: 5
|
||||
**Estimated effort**: 2-3 hours
|
||||
|
||||
#### Models to Migrate
|
||||
|
||||
1. `Models/ContentViewModel.swift:12`
|
||||
```swift
|
||||
// Current
|
||||
class ContentViewModel: ObservableObject {
|
||||
@Published var items: [Item] = []
|
||||
@Published var isLoading = false
|
||||
}
|
||||
|
||||
// Migrated
|
||||
@Observable
|
||||
class ContentViewModel {
|
||||
var items: [Item] = []
|
||||
var isLoading = false
|
||||
}
|
||||
```
|
||||
|
||||
2. `Models/UserSettings.swift:8`
|
||||
[Similar migration...]
|
||||
|
||||
#### Views to Update After Model Migration
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `Views/ContentView.swift:15` | `@StateObject` → `@State` |
|
||||
| `Views/ItemList.swift:23` | `@ObservedObject` → plain property |
|
||||
| `Views/SettingsView.swift:8` | `@EnvironmentObject` → `@Environment` |
|
||||
|
||||
### @EnvironmentObject → @Environment
|
||||
|
||||
- `Views/RootView.swift:45`
|
||||
```swift
|
||||
// Current
|
||||
.environmentObject(settings)
|
||||
|
||||
// Migrated
|
||||
.environment(settings)
|
||||
```
|
||||
|
||||
- `Views/SettingsView.swift:12`
|
||||
```swift
|
||||
// Current
|
||||
@EnvironmentObject var settings: AppSettings
|
||||
|
||||
// Migrated
|
||||
@Environment(AppSettings.self) var settings
|
||||
```
|
||||
|
||||
## MEDIUM Priority Migrations
|
||||
|
||||
### Deprecated onChange Modifier
|
||||
|
||||
- `Views/SearchView.swift:34`
|
||||
```swift
|
||||
// Deprecated
|
||||
.onChange(of: query) { newValue in
|
||||
search(newValue)
|
||||
}
|
||||
|
||||
// Modern
|
||||
.onChange(of: query) { oldValue, newValue in
|
||||
search(newValue)
|
||||
}
|
||||
```
|
||||
|
||||
### async/await Opportunities
|
||||
|
||||
- `Services/NetworkService.swift` - 3 completion handler methods
|
||||
- `fetchUser(completion:)` → `fetchUser() async throws`
|
||||
- `fetchItems(completion:)` → `fetchItems() async throws`
|
||||
- `uploadData(completion:)` → `uploadData() async throws`
|
||||
|
||||
## Migration Order
|
||||
|
||||
1. **First**: Migrate models to `@Observable`
|
||||
- All `ObservableObject` → `@Observable`
|
||||
- Remove all `@Published`
|
||||
|
||||
2. **Second**: Update view property wrappers
|
||||
- `@StateObject` → `@State` (for owned models)
|
||||
- `@ObservedObject` → plain or `@Bindable`
|
||||
- `@EnvironmentObject` → `@Environment`
|
||||
|
||||
3. **Third**: Update view modifiers
|
||||
- `.environmentObject()` → `.environment()`
|
||||
- Deprecated `onChange` syntax
|
||||
|
||||
4. **Fourth**: Adopt async/await (optional, but recommended)
|
||||
|
||||
## Breaking Changes Warning
|
||||
|
||||
⚠️ **Deployment Target**: Full migration requires iOS 17+
|
||||
|
||||
If you need to support iOS 16 or earlier:
|
||||
- Keep `ObservableObject` for those models
|
||||
- Use conditional compilation:
|
||||
```swift
|
||||
#if os(iOS) && swift(>=5.9)
|
||||
@Observable
|
||||
class ViewModel { ... }
|
||||
#else
|
||||
class ViewModel: ObservableObject { ... }
|
||||
#endif
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After migration:
|
||||
1. Build and fix any compiler errors
|
||||
2. Test view updates (properties should still trigger UI refresh)
|
||||
3. Test bindings (TextField, Toggle still work)
|
||||
4. Test environment injection
|
||||
```
|
||||
|
||||
## When No Migration Needed
|
||||
|
||||
```markdown
|
||||
# Modernization Analysis Results
|
||||
|
||||
## Summary
|
||||
Codebase is already using modern patterns!
|
||||
|
||||
## Verified
|
||||
- ✅ Using `@Observable` macro
|
||||
- ✅ Using `@State` with Observable models
|
||||
- ✅ Using `@Environment` for shared state
|
||||
- ✅ No deprecated modifiers detected
|
||||
|
||||
## Optional Improvements
|
||||
- Consider adopting iOS 18+ features when available
|
||||
- Review remaining completion handlers for async/await conversion
|
||||
```
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Is model a class with published properties?
|
||||
├─ YES: Does it conform to ObservableObject?
|
||||
│ ├─ YES: Target iOS 17+?
|
||||
│ │ ├─ YES → Migrate to @Observable
|
||||
│ │ └─ NO → Keep ObservableObject
|
||||
│ └─ NO: Already modern or not observable
|
||||
└─ NO: Check if it's a struct (usually fine)
|
||||
|
||||
Is view using @StateObject?
|
||||
├─ YES: Is the model @Observable?
|
||||
│ ├─ YES → Change to @State
|
||||
│ └─ NO → Keep @StateObject until model migrated
|
||||
└─ NO: Check other wrappers
|
||||
|
||||
Is view using @ObservedObject?
|
||||
├─ YES: Is the model @Observable?
|
||||
│ ├─ YES: Need binding?
|
||||
│ │ ├─ YES → Use @Bindable
|
||||
│ │ └─ NO → Remove wrapper, use plain property
|
||||
│ └─ NO → Keep @ObservedObject
|
||||
└─ NO: Already modern
|
||||
|
||||
Is view using @EnvironmentObject?
|
||||
├─ YES: Is the model @Observable?
|
||||
│ ├─ YES → Change to @Environment(Type.self)
|
||||
│ └─ NO → Keep @EnvironmentObject
|
||||
└─ NO: Already modern
|
||||
```
|
||||
|
||||
## False Positives to Avoid
|
||||
|
||||
**Not issues**:
|
||||
- Third-party SDK types using ObservableObject
|
||||
- Models that intentionally support iOS 14-16
|
||||
- Combine publishers (not the same as @Published)
|
||||
- Already migrated code using @Observable
|
||||
- Apple protocol families unrelated to Observation — classes conforming to `AppIntent`, `EntityQuery`, `AppEntity`, `WidgetConfiguration`, `TimelineProvider`, or other App Intents / WidgetKit protocols are NOT `ObservableObject` and should not be flagged for `@Observable` migration
|
||||
|
||||
**Check before reporting**:
|
||||
- Verify file is in your project, not dependencies
|
||||
- Check deployment target constraints
|
||||
- Confirm model is actually used in SwiftUI views
|
||||
- Confirm the class actually conforms to `ObservableObject` — do not flag classes just because they are classes
|
||||
3
.claude/skills/axiom-modernize/agents/openai.yaml
Normal file
3
.claude/skills/axiom-modernize/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Modernize"
|
||||
short_description: "The user wants to modernize iOS code to iOS 17/18 patterns, migrate from ObservableObject to @Observable, update @Sta..."
|
||||
Reference in New Issue
Block a user