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