ios-swiftui — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-swiftui (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.
@StateObject is the pre-iOS 17 pattern.NavigationLink + .navigationDestination.List for very large datasets (cell reuse + prefetching)..task modifier for async work.@ViewBuilder or Group instead.condition ? viewA : viewB), not if/else that changes the view type in ways that break animations.body often; smaller views = smaller re-evaluation scope.| Scenario | iOS 17+ | Pre-iOS 17 |
|---|---|---|
| Simple value owned by view | @State | @State |
| Pass value down for read/write | @Binding | @Binding |
| Reference-type model owned by view | @State + @Observable | @StateObject + ObservableObject |
| Reference-type model passed in | just pass it (auto-tracked) | @ObservedObject |
| Shared model via environment | @Environment + custom key | @EnvironmentObject |
| Create binding to @Observable property | @Bindable | N/A (use @Published + $) |
| System environment values | @Environment(\.colorScheme) | same |
Key insight: With @Observable, SwiftUI tracks which properties a view actually reads in body. With ObservableObject, ANY @Published change triggers ALL observing views to re-evaluate.
| Need | Use | Why |
|---|---|---|
| Small fixed list of items | VStack / HStack | All children measured upfront, correct sizing |
| Large scrollable list (100+) | LazyVStack in ScrollView | Creates views on demand |
| Very large list (1000+) with editing | List | Cell reuse, prefetching, swipe actions |
| 2D grid layout | LazyVGrid / LazyHGrid | Flexible column/row definitions |
| Aligned rows + columns (small data) | Grid + GridRow (iOS 16+) | Alignment across rows |
| Responsive layout | ViewThatFits (iOS 16+) | Picks first child that fits |
| Need | Use |
|---|---|
| Linear drill-down (push/pop) | NavigationStack with path |
| Master-detail (iPad) | NavigationSplitView |
| Tab-based app | TabView (iOS 18: Tab items) |
| Modal presentation | .sheet, .fullScreenCover |
| Programmatic deep linking | NavigationStack(path:) + NavigationPath |
| Need | Use |
|---|---|
| Complex form/detail | .sheet or .fullScreenCover |
| Simple yes/no question | .alert |
| Choose from options | .confirmationDialog |
| Contextual info (iPad) | .popover |
| Half-height sheet | .sheet + .presentationDetents([.medium]) |
| Non-dismissable sheet | .interactiveDismissDisabled(true) |
@Observable
class UserViewModel {
var name = ""
var email = ""
var isLoading = false
@ObservationIgnored // not tracked
var internalCache: [String: Any] = [:]
func load() async {
isLoading = true
defer { isLoading = false }
// fetch data...
}
}
struct UserView: View {
@State private var viewModel = UserViewModel()
var body: some View {
Form {
TextField("Name", text: $viewModel.name) // needs @Bindable or use @State
}
.task { await viewModel.load() }
}
}Note: To get $viewModel.name binding from @State, you can access it directly. If the model is passed as a parameter (not @State), wrap with @Bindable:
struct EditView: View {
@Bindable var viewModel: UserViewModel
var body: some View {
TextField("Name", text: $viewModel.name)
}
}struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: item) {
Text(item.title)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item, path: $path)
}
.navigationTitle("Items")
}
}
}struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background, in: .rect(cornerRadius: 12))
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}struct PostListView: View {
@State private var posts: [Post] = []
@State private var error: Error?
var body: some View {
List(posts) { post in
Text(post.title)
}
.overlay {
if posts.isEmpty && error == nil {
ProgressView()
}
if let error {
ContentUnavailableView("Error", systemImage: "exclamationmark.triangle",
description: Text(error.localizedDescription))
}
}
.task {
do {
posts = try await api.fetchPosts()
} catch {
self.error = error
}
}
}
}Use this table to decide which reference file to read for a given task:
| Task / Question | Read |
|---|---|
| Building layouts, stacks, grids | references/layout.md |
| ScrollView, List, ForEach, lazy containers | references/layout.md |
| @State, @Binding, @Observable, @Environment | references/state.md |
| ObservableObject, @Published, migration to @Observable | references/state.md |
| NavigationStack, NavigationSplitView, deep linking | references/navigation.md |
| Sheets, alerts, popovers, modals | references/navigation.md |
| TabView, toolbar, searchable | references/navigation.md |
| Animations, transitions, springs | references/animation.md |
| matchedGeometryEffect, hero animations | references/animation.md |
| PhaseAnimator, KeyframeAnimator | references/animation.md |
| Symbol effects, haptics | references/animation.md |
| ViewModifier, @ViewBuilder, PreferenceKey | references/patterns.md |
| GeometryReader, coordinate spaces | references/patterns.md |
| App lifecycle, scenePhase, .task, .onAppear | references/patterns.md |
| UIKit interop, UIViewRepresentable | references/patterns.md |
| Performance optimization | references/patterns.md |
| AsyncImage, #Preview, ContentUnavailableView | references/patterns.md |
@StateObject is for ObservableObject only. Use @State with @Observable.NavigationStack — causes double nav bars and broken navigation.NavigationStack or NavigationSplitView.body is called frequently. Move computation to .task, .onAppear, or the model.@ViewBuilder or Group..frame(), or containerRelativeFrame instead.Identifiable or explicit id:..task instead; it auto-cancels when the view disappears.@State initializes once. If you need to react to parameter changes, use .onChange(of:) or derive state differently.| Feature | Minimum iOS |
|---|---|
@Observable, @Bindable | 17 |
NavigationStack, NavigationSplitView | 16 |
Grid, GridRow | 16 |
ViewThatFits | 16 |
PhaseAnimator, KeyframeAnimator | 17 |
sensoryFeedback | 17 |
SymbolEffect | 17 |
ContentUnavailableView | 17 |
#Preview macro | 17 |
containerRelativeFrame | 17 |
scrollPosition, scrollTargetBehavior | 17 |
withAnimation completion | 17 |
navigationDestination(item:) | 17 |
Tab type in TabView | 18 |
@Entry macro for Environment | 18 |
.presentationSizing | 18 |
MeshGradient | 18 |
MyApp/
├── MyAppApp.swift // @main App struct
├── Models/ // Data models, @Observable classes
├── Views/
│ ├── Components/ // Reusable small views
│ ├── Screens/ // Full-screen views
│ └── Modifiers/ // Custom ViewModifiers
├── Services/ // Network, persistence, etc.
├── Extensions/ // View+, Color+, etc.
└── Resources/ // Assets, LocalizableProfileView, SettingsScreen, UserRow)<Feature>ViewModel with @ObservableCardModifier, PrimaryButtonStyle)some View return type, never concrete view types in public API~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.