Skip to content
Mobile
Skill

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

From plugin
axiom
1.3k71 skills44 agents18 commands1 MCP

Claude Code

$ npx -y skills add charleswiltgen/axiom --skill axiom-modernize --agent claude-code

SKILL.md

axiom-modernize.SKILL.md
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

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)

**Why migrate**: Deprecated mo

Read more
@charleswiltgen
Ships withaxiom

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.

1.3k71 skills44 agents1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
1.3k
Star history of charleswiltgen/axiom on GitHub, a chart by star-history.com

Other skills on axiom.

axiom-accessibility

axiom-accessibility

Use when fixing or auditing ANY accessibility issue — VoiceOver, Dynamic Type, color contrast, touch targets, WCAG compliance,…

axiom-ai

axiom-ai

Use when implementing, testing, or evaluating ANY Apple Intelligence, on-device AI, or speech-to-text feature. Covers Foundation…

axiom-analyze-crash

axiom-analyze-crash

Use when the user has a crash log (.ips, MetricKit JSON, legacy .crash text, .xccrashpoint bundle, or pasted text) that needs…

More skills from other plugins.

deep-code-review

deep-code-review

In-depth design-focused code review - understands codebase context before evaluating PR changes, posts structured feedback to…

expo4d53k
@expo@expoInstall Skill
modlens

modlens

Plug-in vision for text-only models. Hard rule: when a file path or URL with an image extension (.png, .jpg, .jpeg, .webp, .gif,…

modlens3w4.6k
@liustack@liustackInstall Skill
appllama-app-design-skill

appllama-app-design-sk…

Build native-feeling, benchmark-quality mobile app screens (Expo / React Native). Use when designing or implementing any mobile…

appllama2mo3.9k
@appllama@appllamaInstall Skill

Questions about axiom-modernize.

Which coding agents can use axiom-modernize?
axiom-modernize installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.