From swiftui-dev
Modern iOS 18+/macOS 15+ development standards using Swift 6, SwiftUI, SwiftData, and the Observation framework. Use when building or reviewing Swift applications, generating SwiftUI views, implementing data persistence, or architecting Apple platform apps. Enforces 2025/2026 best practices and rejects legacy patterns (ObservableObject, @Published, CoreData, @StateObject). Covers state management, concurrency, persistence, navigation, and testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/swiftui-dev:modern-apple-devThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Target:** iOS 18+, macOS 15+ | Swift 6 strict concurrency mode
references/CLAUDE.mdreferences/architecture.mdreferences/concurrency.mdreferences/liquid-glass.mdreferences/navigation-patterns.mdreferences/state-management.mdreferences/swiftdata.mdreferences/testing.mdreferences/ui-patterns.mdscripts/CLAUDE.mdscripts/accessibility_audit.pyscripts/legacy_pattern_detector.pyscripts/validate_accessibility.pyscripts/validate_patterns.pyTarget: iOS 18+, macOS 15+ | Swift 6 strict concurrency mode Stack: SwiftUI, SwiftData, Swift Concurrency (Strict), Observation Framework
Scripts location: $CLAUDE_PLUGIN_ROOT/skills/modern-apple-dev/scripts/
Before generating or reviewing code, verify:
| Pattern | Action |
|---|---|
ObservableObject | REJECT → Use @Observable |
@Published | REJECT → Properties observed by default |
CoreData | REJECT → Use SwiftData |
@StateObject | REJECT → Use @State |
DispatchQueue.main.async | REJECT → Use Swift Concurrency |
Cross-actor data not Sendable | REJECT → Require conformance |
Interactive UI without .accessibilityIdentifier() | REJECT → Add identifier |
NavigationView | REJECT → Use NavigationStack |
Every interactive UI element MUST have an accessibility identifier:
// Naming: {screen}_{element}_{descriptor}
Button("Submit") { }.accessibilityIdentifier("login_button_submit")
TextField("Email", text: $email).accessibilityIdentifier("login_textfield_email")
Toggle("Dark Mode", isOn: $darkMode).accessibilityIdentifier("settings_toggle_darkMode")
// Dynamic list items include ID
ForEach(items) { item in
ItemRow(item: item)
.accessibilityIdentifier("home_cell_item_\(item.id)")
}
// Screen containers for existence checks
var body: some View {
VStack { /* content */ }
.accessibilityIdentifier("screen_login")
}
@Observable
final class UserProfile {
var name: String = "Guest"
var isPremium: Bool = false
@ObservationIgnored var cache: [String: Any] = [:] // Won't trigger UI updates
}
struct ProfileView: View {
@State var profile = UserProfile() // @State manages reference types
var body: some View {
Text(profile.name) // Only redraws when 'name' changes
.accessibilityIdentifier("profile_label_name")
EditView(profile: profile)
}
}
struct EditView: View {
@Bindable var profile: UserProfile // Creates $profile.name bindings
var body: some View {
TextField("Name", text: $profile.name)
.accessibilityIdentifier("profile_textfield_name")
}
}
@Observable
@MainActor
final class FeatureViewModel {
// MARK: - State
var items: [Item] = []
var isLoading = false
var error: Error?
// MARK: - Dependencies
private let service: ItemServiceProtocol
init(service: ItemServiceProtocol = ItemService()) {
self.service = service
}
// MARK: - Actions
func load() async {
isLoading = true
defer { isLoading = false }
do {
items = try await service.fetchItems()
} catch {
self.error = error
}
}
}
struct FeatureView: View {
@State private var viewModel = FeatureViewModel()
var body: some View {
content
.task { await viewModel.load() }
.accessibilityIdentifier("screen_feature")
}
@ViewBuilder
private var content: some View {
if viewModel.isLoading {
ProgressView()
.accessibilityIdentifier("feature_progress_loading")
} else {
List(viewModel.items) { item in
ItemRow(item: item)
.accessibilityIdentifier("feature_row_\(item.id)")
}
}
}
}
@Model
final class TodoItem {
var title: String
var isDone: Bool
var createdAt: Date
init(title: String, isDone: Bool = false) {
self.title = title
self.isDone = isDone
self.createdAt = .now
}
}
struct TodoListView: View {
@Query(sort: \.createdAt, order: .reverse) private var items: [TodoItem]
@Environment(\.modelContext) private var context
var body: some View {
List {
ForEach(items) { item in
Toggle(item.title, isOn: Bindable(item).isDone)
.accessibilityIdentifier("todo_toggle_\(item.id)")
}
.onDelete { offsets in
offsets.forEach { context.delete(items[$0]) }
// No save() needed - autosaves
}
}
.accessibilityIdentifier("screen_todoList")
}
}
@MainActor
@Observable
class DataModel {
var items: [String] = []
func loadData() async {
let fetched = await fetchFromNetwork() // Off main thread
self.items = fetched // Back on MainActor after await
}
nonisolated func pureLogic(_ input: [String]) -> [String] {
input.filter { $0.count > 5 }
}
}
// Sendable requirement for cross-actor data
struct SharedData: Sendable {
let id: UUID
let value: String
}
// Actor for thread-safe state
actor DataStore {
private var cache: [String: Data] = [:]
func store(_ data: Data, for key: String) {
cache[key] = data
}
func retrieve(for key: String) -> Data? {
cache[key]
}
}
@Observable
class Router {
var path = NavigationPath()
func navigate(to route: AppRoute) {
path.append(route)
}
func pop() {
guard !path.isEmpty else { return }
path.removeLast()
}
}
enum AppRoute: Hashable {
case detail(Item)
case settings
}
struct AppRoot: View {
@State var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .detail(let item):
DetailView(item: item)
case .settings:
SettingsView()
}
}
}
.environment(router)
.accessibilityIdentifier("screen_root")
}
}
Is this a simple screen with basic CRUD?
├─ YES → View owns state directly, use @Query
└─ NO → Does it need shared/complex state?
├─ YES → Extract to @MainActor @Observable class
└─ NO → Keep logic in View
Need to share code with non-SwiftUI target?
├─ YES → Create repository abstraction
└─ NO → Use @Query directly (more performant)
Run these scripts to audit Swift code for compliance:
Finds rejected patterns (ObservableObject, @Published, CoreData, DispatchQueue, etc.):
python $CLAUDE_PLUGIN_ROOT/skills/modern-apple-dev/scripts/legacy_pattern_detector.py <path> # Audit file or directory
python $CLAUDE_PLUGIN_ROOT/skills/modern-apple-dev/scripts/legacy_pattern_detector.py <path> --fix # Show migration suggestions
python $CLAUDE_PLUGIN_ROOT/skills/modern-apple-dev/scripts/legacy_pattern_detector.py <path> --json # Machine-readable output
Finds interactive UI elements missing .accessibilityIdentifier():
python $CLAUDE_PLUGIN_ROOT/skills/modern-apple-dev/scripts/accessibility_audit.py <path> # Audit file or directory
python $CLAUDE_PLUGIN_ROOT/skills/modern-apple-dev/scripts/accessibility_audit.py <path> --fix # Show suggested identifiers
python $CLAUDE_PLUGIN_ROOT/skills/modern-apple-dev/scripts/accessibility_audit.py <path> --json # Machine-readable output
Workflow: Run both scripts before code review or PR submission to catch issues early.
PersistentIdentifier insteadcontext.save() unless sharing data to extensions.accessibilityIdentifier().accessibilityIdentifier("screen_[name]")npx claudepluginhub jbcrane13/plugin-market --plugin swiftui-devGuides iOS app architecture using Swift 6, iOS 18+, SwiftUI, SwiftData, Observation framework, and concurrency. Modernizes legacy patterns like Core Data, ObservableObject, and GCD.
Provides expert Swift and SwiftUI guidance for iOS/macOS development using iOS 17+ patterns like @Observable, MVVM/TCA architecture, property wrappers, and production-ready code.
Swift 6 fundamentals for all Apple platforms. Use when implementing concurrency, architecture, testing, i18n, or performance optimization across iOS, macOS, iPadOS, watchOS, visionOS.