Comprehensive iOS app development skill with Liquid Glass design, modern Swift patterns (@Observable, SwiftData), and App Store readiness. Self-updates by checking Apple Developer News for new iOS versions. Use for any iOS/SwiftUI project.
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
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.
Apple deprecates APIs and mandates new patterns yearly. iOS 26 made Liquid Glass mandatory. iOS 27 will remove the option to retain old designs.
| Layer | Technology | NOT This |
|---|---|---|
| State | @Observable | |
| Bindings | @State, @Bindable | |
| Persistence | SwiftData + VersionedSchema | |
| UI | SwiftUI + Liquid Glass | |
| Architecture | MVVM with @Query |
// 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))
Liquid Glass is Apple's translucent material that reflects and refracts surroundings while dynamically transforming to bring focus to content.
MANDATORY by iOS 27 - Apple will remove option to retain old designs.
// 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)
Group elements to blend together:
GlassEffectContainer {
HStack(spacing: 12) {
Button(action: {}) {
Image(systemName: "camera")
}
.glassEffect()
Button(action: {}) {
Image(systemName: "photo")
}
.glassEffect()
}
}
Link related elements across view hierarchy:
@Namespace private var glassNamespace
VStack {
Button("Expand") { }
.glassEffectID("action", in: glassNamespace)
if expanded {
DetailPanel()
.glassEffectID("action", in: glassNamespace)
}
}
// Circular glass
Circle()
.glassEffect()
.frame(width: 60, height: 60)
// Custom shape
CustomPath()
.glassEffect(.regular, in: CustomShape())
.sheet(isPresented: $showSheet) {
ContentView()
.presentationDetents([.medium, .large])
// Auto-gets Liquid Glass background
}
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))
}
}
}
}
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()
}
}
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)))
}
}
}
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)
}
}
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?
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] { [] }
}
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
)
}
Lightweight (automatic):
@Attribute(originalName:)Crashes if you:
.unique when duplicates exist@Attribute(originalName:)// 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:
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)
}
}
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
Run before EVERY App Store submission.
YOUR_TEAM_IDPrivacyInfo.xcprivacy exists (required since May 2024)| Pattern | Why Deprecated | Use Instead |
|---|---|---|
ObservableObject | Legacy, unnecessary re-renders | @Observable macro |
@Published | Requires ObservableObject | Properties on @Observable |
@StateObject | For ObservableObject | @State with @Observable |
@ObservedObject | For ObservableObject | Direct access or @Bindable |
| Core Data | Legacy persistence | SwiftData |
UserDefaults for models | Not type-safe | SwiftData |
| Unversioned SwiftData | Migration crashes | VersionedSchema from day one |
UIKit wrappers | When SwiftUI native exists | Native SwiftUI |
| Custom tab bars | Poor Liquid Glass support | Native TabView |
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
}
}
// In UIViewController
let hostingController = UIHostingController(rootView: SwiftUIToolbar())
hostingController.sizingOptions = [.intrinsicContentSize]
hostingController.view.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: hostingController.view)
CABackdropLayer - GPU intensive// 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]
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
}
# 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"
YOUR_TEAM_ID@Observable for all ViewModels@State for ViewModels in Views.glassEffect() only to overlay elementsPrivacyInfo.xcprivacy