ios-development — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-development (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.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.
| Layer | Technology | NOT This |
|---|---|---|
| State | @Observable | ~~ObservableObject~~ |
| Bindings | @State, @Bindable | ~~@StateObject, @ObservedObject~~ |
| Persistence | SwiftData + VersionedSchema | ~~Core Data, UserDefaults~~ |
| UI | SwiftUI + Liquid Glass | ~~UIKit wrappers~~ |
| Architecture | MVVM with @Query | ~~MVC~~ |
// 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 2024Run 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~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.