Get the FREE Ultimate OpenClaw Setup Guide →

ios-development

npx machina-cli add skill simonstrumse/vibelabs-skills/ios-development --openclaw
Files (1)
SKILL.md
19.3 KB

iOS Development Skill

Complete iOS development toolkit: design system, architecture patterns, and App Store verification. Self-updating - checks Apple Developer News for iOS changes.

Team ID: YOUR_TEAM_ID Current Target: iOS 26 (Liquid Glass) Minimum: iOS 17.0 (for @Observable, SwiftData) Last Updated: December 2025


Self-Update Protocol

When starting a new iOS project or major feature, ALWAYS run this check:

BEFORE implementing iOS features, search the web for:
1. "Apple Developer News iOS [current year]"
2. "iOS [latest version] SwiftUI new APIs"
3. "WWDC [current year] SwiftUI changes"

Check for:
- New iOS version announcements
- Deprecated APIs
- New required APIs (privacy, permissions)
- Design system changes
- App Store requirement changes

If significant changes found, update this skill file before proceeding.

Why self-update? Apple deprecates APIs and mandates new patterns yearly. iOS 26 made Liquid Glass mandatory. iOS 27 will remove the option to retain old designs.


Quick Reference

Modern Swift Stack (iOS 17+)

LayerTechnologyNOT This
State@ObservableObservableObject
Bindings@State, @Bindable@StateObject, @ObservedObject
PersistenceSwiftData + VersionedSchemaCore Data, UserDefaults
UISwiftUI + Liquid GlassUIKit wrappers
ArchitectureMVVM with @QueryMVC

Glass Effect Quick Reference

// Basic glass
.glassEffect()

// Tinted + interactive
.glassEffect(.regular.tint(.blue).interactive())

// Group related elements
GlassEffectContainer { ... }

// Morphing sheet
.matchedTransitionSource(id: "x", in: namespace)
.navigationTransition(.zoom(sourceID: "x", in: namespace))

PART 1: LIQUID GLASS DESIGN SYSTEM

Design Philosophy

Liquid Glass is Apple's translucent material that reflects and refracts surroundings while dynamically transforming to bring focus to content.

Core Principles

  1. Dynamic Transparency - Interfaces feel like overlapping sheets of glass
  2. Glass is Overlay - Apply ONLY to elements on TOP of UI (toolbars, FABs, modals)
  3. Fluid Interaction - Morphing transitions connect related elements
  4. Depth and Context - Elements create sense of spatial hierarchy
  5. Colorful Backgrounds - Vibrant content behind glass makes effects shine

Layout Rules

  • Keep essential UI in central 70% of canvas
  • Let tab bars auto-collapse on scroll
  • Think in layers: content at bottom, glass controls on top
  • Apply glass sparingly (key surfaces only)
  • Use native Apple components (optimized for Liquid Glass)

What NOT to Do

  • Don't create custom components (poor Liquid Glass compatibility)
  • Don't align thin borders exactly with mask edges
  • Don't overuse glass effects
  • Don't apply glass to main content areas
  • Don't stack multiple glass layers

MANDATORY by iOS 27 - Apple will remove option to retain old designs.


Glass Effect APIs

Basic Glass Effect

// Simple glass background
Button("Action") { }
    .glassEffect()

// Tinted glass with color
Button("Save") { }
    .glassEffect(.regular.tint(.blue))

// Interactive glass (grows + shimmers on touch)
Button("Tap Me") { }
    .glassEffect(.regular.tint(.purple.opacity(0.8)).interactive())

// Button style alternative
Button("Glass Button") { }
    .buttonStyle(.glass)

Glass Effect Container

Group elements to blend together:

GlassEffectContainer {
    HStack(spacing: 12) {
        Button(action: {}) {
            Image(systemName: "camera")
        }
        .glassEffect()

        Button(action: {}) {
            Image(systemName: "photo")
        }
        .glassEffect()
    }
}

Glass Effect Identity

Link related elements across view hierarchy:

@Namespace private var glassNamespace

VStack {
    Button("Expand") { }
        .glassEffectID("action", in: glassNamespace)

    if expanded {
        DetailPanel()
            .glassEffectID("action", in: glassNamespace)
    }
}

Custom Glass Shapes

// Circular glass
Circle()
    .glassEffect()
    .frame(width: 60, height: 60)

// Custom shape
CustomPath()
    .glassEffect(.regular, in: CustomShape())

Liquid Glass Sheets

Partial Height (Floating)

.sheet(isPresented: $showSheet) {
    ContentView()
        .presentationDetents([.medium, .large])
        // Auto-gets Liquid Glass background
}

Morphing Transitions

struct ContentView: View {
    @Namespace private var transition
    @State private var showInfo = false

    var body: some View {
        NavigationStack {
            MainContent()
                .toolbar {
                    ToolbarItem(placement: .bottomBar) {
                        Button("Info", systemImage: "info") {
                            showInfo = true
                        }
                    }
                    .matchedTransitionSource(id: "info", in: transition)
                }
                .sheet(isPresented: $showInfo) {
                    InfoView()
                        .presentationDetents([.medium, .large])
                        .navigationTransition(.zoom(sourceID: "info", in: transition))
                }
        }
    }
}

Component Patterns

Floating Action Bar

struct FloatingActionBar: View {
    let actions: [ActionItem]

    var body: some View {
        GlassEffectContainer {
            HStack(spacing: 16) {
                ForEach(actions) { action in
                    Button(action: action.handler) {
                        Image(systemName: action.icon)
                            .font(.title2)
                    }
                    .glassEffect(.regular.interactive())
                }
            }
            .padding(.horizontal, 20)
            .padding(.vertical, 12)
        }
        .glassEffect()
    }
}

Glass Card

struct GlassCard<Content: View>: View {
    @ViewBuilder let content: Content

    var body: some View {
        content
            .padding(20)
            .background {
                RoundedRectangle(cornerRadius: 24)
                    .glassEffect(.regular.tint(.white.opacity(0.1)))
            }
    }
}

Animated Counter

struct GlassCounter: View {
    let value: Int

    var body: some View {
        Text("\(value)")
            .font(.system(size: 48, weight: .bold, design: .rounded))
            .contentTransition(.numericText())
            .padding(24)
            .glassEffect(.regular.tint(.blue.opacity(0.3)).interactive())
            .animation(.spring(duration: 0.3), value: value)
    }
}

PART 2: MODERN SWIFT ARCHITECTURE

State Management: @Observable

CRITICAL: Always use @Observable (iOS 17+). ObservableObject is legacy.

import Observation

@Observable
final class MyViewModel {
    var title: String = ""
    var items: [Item] = []
    var isLoading: Bool = false

    // NO @Published needed!
    // Views auto-update when properties they READ change

    func loadItems() async {
        isLoading = true
        items = await fetchItems()
        isLoading = false
    }
}

In Views:

struct MyView: View {
    @State private var viewModel = MyViewModel()  // NOT @StateObject

    var body: some View {
        List(viewModel.items) { item in
            Text(item.name)
        }
        .task {
            await viewModel.loadItems()
        }
    }
}

Why @Observable?

  • Performance: Views only re-render when properties they actually read change
  • Simplicity: No @Published, @ObservedObject, @StateObject boilerplate
  • Apple recommended: "For new development, using Observable is the easiest way"

SwiftData: ALWAYS Use VersionedSchema

Never ship unversioned SwiftData. Start with VersionedSchema from day one to avoid migration crashes.

import SwiftData

// ALWAYS start versioned
enum AppSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Item.self, Category.self]
    }

    @Model
    final class Item {
        var id: UUID
        var name: String
        var createdAt: Date

        init(name: String) {
            self.id = UUID()
            self.name = name
            self.createdAt = Date()
        }
    }

    @Model
    final class Category {
        var id: UUID
        var name: String

        @Relationship(deleteRule: .cascade)
        var items: [Item] = []

        init(name: String) {
            self.id = UUID()
            self.name = name
        }
    }
}

// Migration Plan (even for V1)
enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [AppSchemaV1.self]
    }

    static var stages: [MigrationStage] { [] }
}

Adding V2 Later

enum AppSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)

    @Model
    final class Item {
        var id: UUID

        // RENAMING: Use @Attribute(originalName:)
        @Attribute(originalName: "name")
        var title: String

        var createdAt: Date

        // NEW PROPERTY: Provide default
        var priority: Int = 0
    }
}

enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [AppSchemaV1.self, AppSchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: AppSchemaV1.self,
        toVersion: AppSchemaV2.self
    )
}

Migration Rules

Lightweight (automatic):

  • Adding new models
  • Adding properties with default values
  • Deleting properties
  • Renaming with @Attribute(originalName:)

Crashes if you:

  • Ship unversioned, then add VersionedSchema
  • Add .unique when duplicates exist
  • Rename without @Attribute(originalName:)

MVVM Pattern with SwiftData

// VIEW: @Query for reactive data
struct ItemListView: View {
    @Query(sort: \Item.createdAt, order: .reverse)
    private var items: [Item]

    @State private var viewModel = ItemListViewModel()
    @Environment(\.modelContext) private var modelContext

    var body: some View {
        List(items) { item in
            ItemRow(item: item)
        }
        .toolbar {
            Button("Add") {
                viewModel.addItem(context: modelContext)
            }
            .glassEffect(.regular.interactive())
        }
    }
}

// VIEWMODEL: Operations only
@Observable
final class ItemListViewModel {
    var newItemName = ""

    func addItem(context: ModelContext) {
        let item = Item(name: newItemName)
        context.insert(item)
        try? context.save()
        newItemName = ""
    }

    func deleteItem(_ item: Item, context: ModelContext) {
        context.delete(item)
        try? context.save()
    }
}

Pattern:

  • @Query in Views for reactive display
  • ViewModels for business logic
  • Pass ModelContext to methods (don't store as property)

App Entry Point

import SwiftUI
import SwiftData

@main
struct MyApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema(versionedSchema: AppSchemaV1.self)
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(
                for: schema,
                migrationPlan: AppMigrationPlan.self,
                configurations: [config]
            )
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

PART 3: PROJECT STRUCTURE

ProjectName/
├── App/
│   ├── ProjectNameApp.swift           # @main, ModelContainer
│   └── AppConfig.swift                # Configuration constants
│
├── Models/
│   ├── Schemas/
│   │   ├── AppSchemaV1.swift          # VersionedSchema V1
│   │   └── AppMigrationPlan.swift     # SchemaMigrationPlan
│   └── Domain/
│       └── DomainModels.swift         # Non-persisted models
│
├── Views/
│   ├── ContentView.swift
│   ├── [Feature]/
│   │   ├── [Feature]View.swift
│   │   └── [Feature]Components.swift
│   └── Components/
│       ├── GlassCard.swift
│       └── FloatingActionBar.swift
│
├── ViewModels/
│   └── [Feature]ViewModel.swift       # @Observable ViewModels
│
├── Services/
│   ├── NetworkService.swift
│   └── [Domain]Service.swift
│
├── Utilities/
│   ├── Extensions/
│   └── Constants.swift
│
└── Resources/
    ├── Assets.xcassets
    ├── Localizable.xcstrings
    └── PrivacyInfo.xcprivacy          # Required since May 2024

PART 4: APP STORE VERIFICATION CHECKLIST

Run before EVERY App Store submission.

TIER 1: Critical (Prevents Rejections)

Build Quality

  • Clean Release build succeeds
  • ZERO warnings in Release configuration
  • BUILD_ACTIVE_ARCHITECTURE_ONLY = NO for Release

Code Signing

  • Team ID: YOUR_TEAM_ID
  • Bundle identifier: reverse-DNS format
  • Provisioning profile valid and not expired
  • Deployment target iOS 17.0+

Privacy & Security

  • PrivacyInfo.xcprivacy exists (required since May 2024)
  • Required reason APIs declared:
    • UserDefaults API
    • File timestamp APIs
    • System boot time APIs
    • Disk space APIs
  • Privacy descriptions in Info.plist
  • All URLs use HTTPS
  • No hardcoded credentials or API keys

Deprecated APIs

  • No UIWebView (use WKWebView)
  • No ObservableObject (use @Observable)
  • No Core Data (use SwiftData)
  • Third-party libraries iOS 26 compatible

Assets

  • App icon 1024x1024px present
  • App icon has NO transparency
  • App icon has NO applied effects
  • Launch screen configured

TIER 2: Quality

Code Quality

  • No placeholder text ("lorem ipsum", "TODO: Replace")
  • No TODO/FIXME comments in production
  • No debug print statements
  • No forced unwraps in production paths

Info.plist

  • CFBundleVersion set
  • CFBundleShortVersionString matches release
  • CFBundleDisplayName set
  • UISupportedInterfaceOrientations set

TIER 3: iOS 26 Specific

  • Built with Xcode 16+
  • Liquid Glass design implemented
  • Essential UI in central 70% of canvas
  • No dependencies blocking iOS 26

PART 5: DEPRECATED PATTERNS

PatternWhy DeprecatedUse Instead
ObservableObjectLegacy, unnecessary re-renders@Observable macro
@PublishedRequires ObservableObjectProperties on @Observable
@StateObjectFor ObservableObject@State with @Observable
@ObservedObjectFor ObservableObjectDirect access or @Bindable
Core DataLegacy persistenceSwiftData
UserDefaults for modelsNot type-safeSwiftData
Unversioned SwiftDataMigration crashesVersionedSchema from day one
UIKit wrappersWhen SwiftUI native existsNative SwiftUI
Custom tab barsPoor Liquid Glass supportNative TabView

PART 6: UIKit INTEGRATION

For camera apps and other UIKit requirements:

// SwiftUI wrapper for UIKit view
struct CameraPreview: UIViewRepresentable {
    let session: AVCaptureSession

    func makeUIView(context: Context) -> PreviewView {
        let view = PreviewView()
        view.videoPreviewLayer.session = session
        view.videoPreviewLayer.videoGravity = .resizeAspectFill
        return view
    }

    func updateUIView(_ uiView: PreviewView, context: Context) {}
}

class PreviewView: UIView {
    override class var layerClass: AnyClass {
        AVCaptureVideoPreviewLayer.self
    }

    var videoPreviewLayer: AVCaptureVideoPreviewLayer {
        layer as! AVCaptureVideoPreviewLayer
    }
}

UIKit Navigation Bar with SwiftUI Content

// In UIViewController
let hostingController = UIHostingController(rootView: SwiftUIToolbar())
hostingController.sizingOptions = [.intrinsicContentSize]
hostingController.view.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: hostingController.view)

PART 7: PERFORMANCE

Glass Effect Performance

  • Glass uses CABackdropLayer - GPU intensive
  • Test on target devices (iPhone XR, SE for baseline)
  • Limit simultaneous glass surfaces to 3-5
  • Avoid glass on rapidly-animating content
  • Profile with Instruments if frame drops occur

SwiftData Performance

// Batch operations
modelContext.autosaveEnabled = false
for item in items {
    modelContext.insert(item)
}
try modelContext.save()
modelContext.autosaveEnabled = true

// Efficient queries with predicates
@Query(filter: #Predicate<Item> { $0.isActive })
private var activeItems: [Item]

@Observable Performance

Views only re-render when properties they actually read change:

@Observable
class ViewModel {
    var title: String = ""      // Change triggers re-render only if view reads title
    var internalState: Int = 0  // No re-render if view doesn't read this
}

PART 8: COMMON COMMANDS

# Build and run
xcodebuild -scheme ProjectName -destination 'platform=iOS Simulator,name=iPhone 16 Pro'

# Run tests
xcodebuild test -scheme ProjectName -destination 'platform=iOS Simulator,name=iPhone 16 Pro'

# Clean build
xcodebuild clean -scheme ProjectName

# Archive for App Store
xcodebuild archive -scheme ProjectName -archivePath build/ProjectName.xcarchive

# Show build settings
xcodebuild -showBuildSettings | grep -E "TEAM|BUNDLE|TARGET"

Quick Start Checklist

  • Set deployment target iOS 17.0 minimum
  • Configure Team ID: YOUR_TEAM_ID
  • Create folder structure
  • Define VersionedSchema from day one
  • Use @Observable for all ViewModels
  • Use @State for ViewModels in Views
  • Use native Apple components (Liquid Glass optimized)
  • Keep essential UI in central 70% of canvas
  • Apply .glassEffect() only to overlay elements
  • Create PrivacyInfo.xcprivacy
  • Test on iOS 26 simulator
  • Run verification checklist before submission

App Store Text Rules

  • Never use emojis in "What to Test" or App Store texts
  • Follow Liquid Glass design for iOS 26+ screenshots
  • Ensure app icon follows current guidelines
  • Test on multiple device sizes

Resources

Source

git clone https://github.com/simonstrumse/vibelabs-skills/blob/main/skills/ios-development/SKILL.mdView on GitHub

Overview

An end-to-end iOS development toolkit focused on Liquid Glass design, MVVM with modern Swift patterns, and App Store readiness. It emphasizes SwiftUI, @Observable, SwiftData, and self-updating checks against Apple Developer News to stay aligned with iOS version changes.

How This Skill Works

Uses a modern Swift stack (SwiftUI, @Observable, SwiftData, @Query) together with a Liquid Glass design system. Persistence uses SwiftData with VersionedSchema and Glass UI patterns; the Self-Update Protocol runs at project start to fetch iOS changes from Apple Developer News and update this skill file if needed.

When to Use It

  • Starting a new iOS project or feature using SwiftUI and the Liquid Glass design system
  • Updating an app to align with App Store requirements and new iOS APIs
  • Building MVVM apps with @Query for data flows
  • Designing Glass UI elements like buttons, containers, and morphing sheets
  • Keeping the project current by running the Self-Update Protocol before major work

Quick Start

  1. Step 1: Review the Self-Update Protocol and Apple Developer News for current year
  2. Step 2: Scaffold a SwiftUI project using MVVM, @Observable, and SwiftData with VersionedSchema
  3. Step 3: Implement the Liquid Glass design system with .glassEffect APIs and verify App Store readiness

Best Practices

  • Follow the Liquid Glass core principles and apply glass only to overlay surfaces
  • Keep essential UI in the central 70% of the canvas and let tabs auto-collapse on scroll
  • Use native Apple components optimized for Liquid Glass; avoid UIKit wrappers
  • Prefer @Observable for state and SwiftData with VersionedSchema for persistence
  • Run the Self-Update Protocol at project start or before major features

Example Use Cases

  • SwiftUI dashboard with glassEffect on overlays and a morphing sheet
  • Photo gallery app using GlassEffectContainer to group controls
  • Settings screen with glass style buttons and responsive tint
  • MVVM app using @Query and SwiftData persistence
  • App Store readiness checklist automated via Apple Developer News updates

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers