/axiom-modernize
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.
$ npx -y skills add charleswiltgen/axiom --skill axiom-modernize --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/axiom-modernize
Context preview
The summary Claude sees to decide when to auto-load this skill.
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.
SKILL.md
axiom-modernize.SKILL.mdname: 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
Tool Use Is Mandatory
Run every Glob, Grep, and Read this prompt lists. Do not reason from training data instead of scanning.
- Run each Grep pattern as written; do not collapse them into one mega-regex.
- Run the Read verifications each section calls for.
- "Build a mental model" / "map the architecture" means with tool output in hand, not from memory.
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
// ❌ 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)
**Do NOT apply this pattern to `GroupSession` (SharePlay/GroupActivities)**
`GroupSession` is a framework-owned `final class` conforming to `ObservableObject`. You cannot redeclare it, and code observing it must keep using Combine — the SDK ships **no** AsyncSequence for `state`, `activity`, or `activeParticipants` (`sessions()` is the only one).
More importantly, `@Published` publishes from `willSet`, so inside a sink the property still holds the **old** value. The standard late-joiner catch-up depends on exactly that timing:
groupSession.$activeParticipants
.sink { activeParticipants in
// groupSession.activeParticipants is still the OLD set here
let newParticipants = activeParticipants.subtracting(groupSession.activeParticipants)
// send current state to joiners only
}Rewriting this against `@Observable` or an AsyncSequence makes `subtracting` return an empty set. There is no crash and no warning — late joiners silently never receive state, and the bug only appears with 3+ participants on a device that joined late. Leave Combine observation of `GroupSession` alone and say why.
Pattern 2: @StateObject → @State (HIGH)
**Why migrate**: Simpler, consistent with value types, works with @Observable
**Requirement**: iOS 17+ with @Observable model
**Detection**:
Grep: @StateObject
// ❌ 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
// ❌ 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\(
// ❌ 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)
Read more
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
Tool Use Is Mandatory
Run every Glob, Grep, and Read this prompt lists. Do not reason from training data instead of scanning.
- Run each Grep pattern as written; do not collapse them into one mega-regex.
- Run the Read verifications each section calls for.
- "Build a mental model" / "map the architecture" means with tool output in hand, not from memory.
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
// ❌ 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)
**Do NOT apply this pattern to `GroupSession` (SharePlay/GroupActivities)**
`GroupSession` is a framework-owned `final class` conforming to `ObservableObject`. You cannot redeclare it, and code observing it must keep using Combine — the SDK ships **no** AsyncSequence for `state`, `activity`, or `activeParticipants` (`sessions()` is the only one).
More importantly, `@Published` publishes from `willSet`, so inside a sink the property still holds the **old** value. The standard late-joiner catch-up depends on exactly that timing:
groupSession.$activeParticipants
.sink { activeParticipants in
// groupSession.activeParticipants is still the OLD set here
let newParticipants = activeParticipants.subtracting(groupSession.activeParticipants)
// send current state to joiners only
}Rewriting this against `@Observable` or an AsyncSequence makes `subtracting` return an empty set. There is no crash and no warning — late joiners silently never receive state, and the bug only appears with 3+ participants on a device that joined late. Leave Combine observation of `GroupSession` alone and say why.
Pattern 2: @StateObject → @State (HIGH)
**Why migrate**: Simpler, consistent with value types, works with @Observable
**Requirement**: iOS 17+ with @Observable model
**Detection**:
Grep: @StateObject
// ❌ 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
// ❌ 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\(
// ❌ 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)
Battle-tested skills, agents, and tools for modern Apple OS development — Swift 6, SwiftUI, Liquid Glass, Apple Intelligence, and more. Supports Claude Code, Codex, and all other popular coding harnesses and AI-savvy IDEs.
Repo: charleswiltgen/axiom
Other skills on axiom.
- /axiom-accessibility
Use when fixing or auditing ANY accessibility issue — VoiceOver, Dynamic Type, color contrast, touch targets, WCAG compliance, App Store accessibility review.
Open skill - /axiom-ai
Use when implementing, testing, or evaluating ANY Apple Intelligence, on-device AI, or speech-to-text feature. Covers Foundation Models, @Generable, LanguageModelSession, Tool protocol, eval suites, model-as-judge scoring, SpeechTranscriber, CoreML.
Open skill - /axiom-analyze-crash
Use when the user has a crash log (.
Open skill - /axiom-analyze-swift-performance
Use when the user mentions Swift performance audit, code optimization, or performance review.
Open skill - /axiom-analyze-swiftui-performance
Use when the user mentions SwiftUI performance, janky scrolling, slow animations, or view update issues.
Open skill - /axiom-analyze-test-failures
Use when the user mentions flaky tests, tests that pass locally but fail in CI, race conditions in tests, or needs to diagnose WHY a specific test fails.
Open skill

