Agents swiftui-data-flow
SwiftUI data flow mechanisms including @Observable, @State, @Binding, and environment values
install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/plugins/frontend/swiftui-dev/skills/swiftui-data-flow" ~/.claude/skills/arustydev-agents-swiftui-data-flow && rm -rf "$T"
manifest:
content/plugins/frontend/swiftui-dev/skills/swiftui-data-flow/SKILL.mdsource content
SwiftUI Data Flow
Purpose
Understanding SwiftUI's data flow mechanisms including @Observable, @State, @Binding, and environment values.
Property Wrappers
@State - Local View State
struct CounterView: View { @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") Button("Increment") { count += 1 } } } }
@Binding - Two-Way Connection
struct ToggleRow: View { let title: String @Binding var isOn: Bool var body: some View { Toggle(title, isOn: $isOn) } } struct SettingsView: View { @State private var notificationsEnabled = false var body: some View { Form { ToggleRow(title: "Notifications", isOn: $notificationsEnabled) } } }
@Observable (iOS 17+/macOS 14+)
@Observable final class UserSettings { var username = "" var notificationsEnabled = true var theme: Theme = .system // Computed properties work automatically var isValid: Bool { !username.isEmpty } } struct SettingsView: View { @State private var settings = UserSettings() var body: some View { Form { TextField("Username", text: $settings.username) Toggle("Notifications", isOn: $settings.notificationsEnabled) Picker("Theme", selection: $settings.theme) { ForEach(Theme.allCases) { theme in Text(theme.rawValue).tag(theme) } } } .disabled(!settings.isValid) } }
@Bindable - Bindings from @Observable
struct EditUserView: View { @Bindable var user: User // User is @Observable var body: some View { Form { TextField("Name", text: $user.name) TextField("Email", text: $user.email) } } }
@Environment - System Values
struct AdaptiveView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.dismiss) private var dismiss var body: some View { VStack { Text("Theme: \(colorScheme == .dark ? "Dark" : "Light")") Button("Close") { dismiss() } } } }
Custom Environment Values
// Define the key private struct APIClientKey: EnvironmentKey { static let defaultValue: APIClient = .live } // Extend EnvironmentValues extension EnvironmentValues { var apiClient: APIClient { get { self[APIClientKey.self] } set { self[APIClientKey.self] = newValue } } } // Usage struct DataView: View { @Environment(\.apiClient) private var api var body: some View { // Use api } } // Injection MyApp() .environment(\.apiClient, .mock)
@Environment with @Observable
@Observable final class AppState { var currentUser: User? var isAuthenticated: Bool { currentUser != nil } } // In App @main struct MyApp: App { @State private var appState = AppState() var body: some Scene { WindowGroup { ContentView() .environment(appState) } } } // In View struct ProfileView: View { @Environment(AppState.self) private var appState var body: some View { if let user = appState.currentUser { Text("Hello, \(user.name)") } } }
Data Flow Patterns
Parent to Child
struct ParentView: View { @State private var data = [Item]() var body: some View { List(data) { item in ChildRow(item: item) // Pass as value } } }
Child to Parent (Binding)
struct FilterView: View { @Binding var selectedFilter: Filter var body: some View { Picker("Filter", selection: $selectedFilter) { ForEach(Filter.allCases) { filter in Text(filter.title).tag(filter) } } } }
Child to Parent (Closure)
struct ItemRow: View { let item: Item let onDelete: () -> Void var body: some View { HStack { Text(item.name) Spacer() Button("Delete", action: onDelete) } } }
Shared State via @Observable
@Observable final class ShoppingCart { var items: [CartItem] = [] var total: Decimal { items.reduce(0) { $0 + $1.price * Decimal($1.quantity) } } func add(_ product: Product) { if let index = items.firstIndex(where: { $0.productId == product.id }) { items[index].quantity += 1 } else { items.append(CartItem(product: product)) } } } // Shared across views via environment struct ProductView: View { @Environment(ShoppingCart.self) private var cart let product: Product var body: some View { Button("Add to Cart") { cart.add(product) } } }
Best Practices
- Use @State for simple local state
- Use @Observable for shared/complex state (iOS 17+)
- Pass data down, actions up
- Prefer @Environment for dependency injection
- Use @Binding sparingly - prefer closures for actions
Migration from ObservableObject
// OLD (pre-iOS 17) class ViewModel: ObservableObject { @Published var items: [Item] = [] } struct MyView: View { @StateObject private var viewModel = ViewModel() } // NEW (iOS 17+) @Observable final class ViewModel { var items: [Item] = [] } struct MyView: View { @State private var viewModel = ViewModel() }
Related Skills
- swiftui-architecture: Overall patterns
- swift-concurrency: Async data flow