Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Default to Model-View (MV) in SwiftUI. Views are lightweight state expressions; models and services own business logic. Do not introduce view models unless the existing code already requires them.
@State, @Environment, @Query, .task, and .onChangeSwiftUI views are structs -- lightweight, disposable, and recreated frequently. Adding a ViewModel means fighting the framework's core design. Apple's own WWDC sessions (Data Flow Through SwiftUI, Data Essentials in SwiftUI, Discover Observation in SwiftUI) barely mention ViewModels.
Every ViewModel adds:
struct FeedView: View {
@Environment(FeedClient.self) private var client
@Environment(AppTheme.self) private var theme
enum ViewState {
case loading, error(String), loaded([Post])
}
@State private var viewState: ViewState = .loading
@State private var isRefreshing = false
var body: some View {
NavigationStack {
List {
switch viewState {
case .loading:
ProgressView("Loading feed...")
.frame(maxWidth: .infinity)
.listRowSeparator(.hidden)
case .error(let message):
ContentUnavailableView("Error", systemImage: "exclamationmark.triangle",
description: Text(message))
.listRowSeparator(.hidden)
case .loaded(let posts):
ForEach(posts) { post in
PostRowView(post: post)
}
}
}
.listStyle(.plain)
.refreshable { await loadFeed() }
.task { await loadFeed() }
}
}
private func loadFeed() async {
do {
let posts = try await client.getFeed()
viewState = .loaded(posts)
} catch {
viewState = .error(error.localizedDescription)
}
}
}SwiftUI modifiers act as small state reducers:
.task(id: searchText) {
guard !searchText.isEmpty else { return }
await searchFeed(query: searchText)
}
.onChange(of: isInSearch, initial: false) {
guard !isInSearch else { return }
Task { await fetchSuggestedFeed() }
}@main
struct MyApp: App {
@State var client = APIClient()
@State var auth = Auth()
@State var router = AppRouter(initialTab: .feed)
var body: some Scene {
WindowGroup {
ContentView()
.environment(client)
.environment(auth)
.environment(router)
}
}
}All dependencies are injected once and available everywhere.
SwiftData was built to work directly in views:
struct BookListView: View {
@Query private var books: [Book]
@Environment(\.modelContext) private var modelContext
var body: some View {
List {
ForEach(books) { book in
BookRowView(book: book)
.swipeActions {
Button("Delete", role: .destructive) {
modelContext.delete(book)
}
}
}
}
}
}Forcing a ViewModel here means manual fetching, manual refresh, and boilerplate everywhere.
If a ViewModel exists in the codebase:
init, then forward them into the ViewModel in the view's init@State in the root view that owns itbootstrapIfNeeded patterns@State private var viewModel: SomeViewModel
init(dependency: Dependency) {
_viewModel = State(initialValue: SomeViewModel(dependency: dependency))
}Modern @Observable ViewModel with child-view binding:
@MainActor @Observable final class ProfileViewModel {
var name: String = ""
var isSaving: Bool = false
private let client: ProfileClient
init(client: ProfileClient) {
self.client = client
}
func save() async throws {
isSaving = true
defer { isSaving = false }
try await client.update(name: name)
}
}
// Owner view creates via @State
struct ProfileScreen: View {
@State private var viewModel: ProfileViewModel
init(client: ProfileClient) {
_viewModel = State(initialValue: ProfileViewModel(client: client))
}
var body: some View {
ProfileForm(viewModel: viewModel)
}
}
// Child view receives and binds
struct ProfileForm: View {
@Bindable var viewModel: ProfileViewModel
var body: some View {
TextField("Name", text: $viewModel.name)
Button("Save") { Task { try? await viewModel.save() } }
.disabled(viewModel.isSaving)
}
}The MV pattern is the default. Introduce a ViewModel only when the view would be hard to read or test without one:
AsyncSequence values with interdependent state transitionsThe bar is "this view would be hard to read and test without a ViewModel," not "I'm used to MVVM."
Use @Environment when the dependency is shared across many views at different depths. Threading it through every intermediate initializer adds noise:
ModelContextUse initializer parameters when the data is specific to this view instance. Makes the view's requirements explicit and keeps previews simple:
Rule of thumb: if three or more intermediate views would need to accept and forward a parameter just to reach a deeply nested consumer, move it to the environment.
Based on guidance from "SwiftUI in 2025: Forget MVVM" (Thomas Ricouard) and Apple WWDC sessions on SwiftUI data flow.
Wire the app shell (TabView + NavigationStack + sheets) and install a global dependency graph (environment objects, services, streaming clients, SwiftData ModelContainer) in one place.
@MainActor
struct AppView: View {
@State private var selectedTab: AppTab = .home
@State private var tabRouter = TabRouter()
var body: some View {
TabView(selection: $selectedTab) {
ForEach(AppTab.allCases) { tab in
let router = tabRouter.router(for: tab)
Tab(value: tab) {
NavigationStack(path: tabRouter.binding(for: tab)) {
tab.makeContentView()
}
.withSheetDestinations(sheet: Binding(
get: { router.presentedSheet },
set: { router.presentedSheet = $0 }
))
.environment(router)
} label: {
tab.label
}
}
}
.tabBarMinimizeBehavior(.onScrollDown)
.withAppDependencyGraph()
}
}@MainActor
enum AppTab: Identifiable, Hashable, CaseIterable {
case home, notifications, settings
var id: String { String(describing: self) }
@ViewBuilder
func makeContentView() -> some View {
switch self {
case .home: HomeView()
case .notifications: NotificationsView()
case .settings: SettingsView()
}
}
@ViewBuilder
var label: some View {
switch self {
case .home: Label("Home", systemImage: "house")
case .notifications: Label("Notifications", systemImage: "bell")
case .settings: Label("Settings", systemImage: "gear")
}
}
}@MainActor
@Observable
final class RouterPath {
var path: [Route] = []
var presentedSheet: SheetDestination?
}
enum Route: Hashable {
case detail(id: String)
}Use a single modifier to install environment objects and handle lifecycle hooks. This keeps wiring consistent and avoids forgetting a dependency at call sites.
extension View {
func withAppDependencyGraph(
client: APIClient = .shared,
auth: Auth = .shared,
theme: Theme = .shared,
toastCenter: ToastCenter = .shared
) -> some View {
environment(client)
.environment(auth)
.environment(theme)
.environment(toastCenter)
.task(id: auth.currentAccount?.id) {
// Re-seed services when account changes
await client.configure(for: auth.currentAccount)
}
}
}Notes:
.task(id:) hooks respond to account/client changes, re-seeding services and watcher state.Install ModelContainer at the root so all feature views share the same store:
extension View {
func withModelContainer() -> some View {
modelContainer(for: [Draft.self, LocalTimeline.self, TagGroup.self])
}
}A single container avoids duplicated stores per sheet or tab and keeps data consistent.
Centralize sheets with a small enum and a helper modifier:
enum SheetDestination: Identifiable {
case composer
case settings
var id: String { String(describing: self) }
}
extension View {
func withSheetDestinations(sheet: Binding<SheetDestination?>) -> some View {
sheet(item: sheet) { destination in
switch destination {
case .composer:
ComposerView().withEnvironments()
case .settings:
SettingsView().withEnvironments()
}
}
}
}Enum-driven sheets keep presentation centralized and testable; adding a new sheet means one enum case and one switch branch.
@main
struct MyApp: App {
@State var client = APIClient()
@State var auth = Auth()
@State var router = AppRouter(initialTab: .home)
var body: some Scene {
WindowGroup {
AppView()
.environment(client)
.environment(auth)
.environment(router)
}
}
}Store NavigationPath as Codable for state restoration. Handle incoming URLs with .onOpenURL:
.onOpenURL { url in
guard let route = Route(from: url) else { return }
router.navigate(to: route)
}See the swiftui-navigation skill for full URL routing patterns.
.task(id:) work is lightweight or cancelled appropriately; long-running work belongs in servicesUse this pattern to keep networking or service dependencies simple and testable without introducing a full view model or heavy DI framework. It works well for SwiftUI apps where you want a small, composable API surface that can be swapped in previews/tests.
struct SomeClient {
var fetchItems: (_ limit: Int) async throws -> [Item]
var search: (_ query: String, _ limit: Int) async throws -> [Item]
}
extension SomeClient {
static func live(baseURL: URL = URL(string: "https://example.com")!) -> SomeClient {
let session = URLSession.shared // Prototyping only. For production, create a URLSession with timeoutIntervalForRequest: 30, timeoutIntervalForResource: 300, waitsForConnectivity: true, and a URLCache.
return SomeClient(
fetchItems: { limit in
// build URL, call session, decode
},
search: { query, limit in
// build URL, call session, decode
}
)
}
}@MainActor
@Observable final class ItemsStore {
enum LoadState { case idle, loading, loaded, failed(String) }
var items: [Item] = []
var state: LoadState = .idle
private let client: SomeClient
init(client: SomeClient) {
self.client = client
}
func load(limit: Int = 20) async {
state = .loading
do {
items = try await client.fetchItems(limit)
state = .loaded
} catch {
state = .failed(error.localizedDescription)
}
}
}struct ContentView: View {
@Environment(ItemsStore.self) private var store
var body: some View {
List(store.items) { item in
Text(item.title)
}
.task { await store.load() }
}
}@main
struct MyApp: App {
@State private var store = ItemsStore(client: .live())
var body: some Scene {
WindowGroup {
ContentView()
.environment(store)
}
}
}init and keep it private..environment for store injection.static func mock(...).self or view state in the client closures.skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references