Effective state management is key to SwiftUI apps. Learn advanced patterns for complex applications.
State Management Patterns
Local State
swift
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}Shared State
swift
class AppState: ObservableObject {
@Published var isLoggedIn = false
@Published var currentUser: User?
@Published var theme: Theme = .light
func login(_ user: User) {
currentUser = user
isLoggedIn = true
}
func logout() {
currentUser = nil
isLoggedIn = false
}
func toggleTheme() {
theme = theme == .light ? .dark : .light
}
}
struct RootView: View {
@StateObject private var state = AppState()
var body: some View {
ContentView()
.environmentObject(state)
}
}
struct ContentView: View {
@EnvironmentObject private var state: AppState
var body: some View {
VStack {
if state.isLoggedIn {
Text("Welcome, \(state.currentUser?.name ?? "")")
Button("Logout") {
state.logout()
}
} else {
LoginView()
}
}
}
}Dependency Injection
Environment Keys
swift
struct APIKey: EnvironmentKey {
static var defaultValue: APIService = APIService()
}
struct DatabaseKey: EnvironmentKey {
static var defaultValue: DatabaseService = DatabaseService()
}
extension EnvironmentValues {
var api: APIService {
get { self[APIKey.self] }
set { self[APIKey.self] = newValue }
}
var database: DatabaseService {
get { self[DatabaseKey.self] }
set { self[DatabaseKey.self] = newValue }
}
}
// Usage
struct ContentView: View {
@Environment(.api) private var api
@Environment(.database) private var database
var body: some View {
VStack {
Text("Content")
}
.task {
await fetchData()
}
}
func fetchData() async {
let data = try? await api.fetchData()
// Use data
}
}Service Injection
swift
protocol DataService {
func fetchItems() async throws -> [Item]
}
class APIDataService: DataService {
func fetchItems() async throws -> [Item] {
// API call
return []
}
}
class MockDataService: DataService {
func fetchItems() async throws -> [Item] {
return [Item(id: "1", name: "Mock")]
}
}
struct ItemsView: View {
@State private var items: [Item] = []
let service: DataService
var body: some View {
List(items) { item in
Text(item.name)
}
.task {
try? await loadItems()
}
}
func loadItems() async throws {
items = try await service.fetchItems()
}
}
// Usage
ItemsView(service: APIDataService())Complex State
State Objects
swift
class FormState: ObservableObject {
@Published var username = ""
@Published var email = ""
@Published var password = ""
@Published var isLoading = false
@Published var errorMessage: String?
var isValid: Bool {
!username.isEmpty && !email.isEmpty && !password.isEmpty
}
var isEmailValid: Bool {
email.contains("@")
}
func reset() {
username = ""
email = ""
password = ""
errorMessage = nil
}
}
struct RegistrationView: View {
@StateObject private var formState = FormState()
var body: some View {
Form {
TextField("Username", text: $formState.username)
TextField("Email", text: $formState.email)
SecureField("Password", text: $formState.password)
if let error = formState.errorMessage {
Text(error)
.foregroundColor(.red)
}
Button("Register") {
Task {
await register()
}
}
.disabled(!formState.isValid || formState.isLoading)
}
}
func register() async {
formState.isLoading = true
defer { formState.isLoading = false }
do {
try await APIService().register(
username: formState.username,
email: formState.email,
password: formState.password
)
} catch {
formState.errorMessage = error.localizedDescription
}
}
}Navigation State
swift
class NavigationState: ObservableObject {
@Published var path = NavigationPath()
func push(_ screen: Screen) {
path.append(screen)
}
func pop() {
if !path.isEmpty {
path.removeLast()
}
}
func popToRoot() {
path.removeLast(path.count)
}
}
enum Screen: Hashable {
case home
case profile
case settings
case detail(id: String)
}
struct AppNavigationView: View {
@StateObject private var navState = NavigationState()
var body: some View {
NavigationStack(path: $navState.path) {
HomeView()
.navigationDestination(for: Screen.self) { screen in
switch screen {
case .home:
HomeView()
case .profile:
ProfileView()
case .settings:
SettingsView()
case .detail(let id):
DetailView(id: id)
}
}
}
.environmentObject(navState)
}
}Reactive Patterns
Combine Integration
swift
import Combine
class SearchViewModel: ObservableObject {
@Published var searchQuery = ""
@Published var results: [SearchResult] = []
@Published var isSearching = false
private var cancellables = Set<AnyCancellable>()
private let service = SearchService()
init() {
setupSearchPipeline()
}
private func setupSearchPipeline() {
$searchQuery
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.flatMap { query in
self.search(query)
}
.sink { [weak self] results in
self?.results = results
self?.isSearching = false
}
.store(in: &cancellables)
}
private func search(_ query: String) -> AnyPublisher<[SearchResult], Never> {
guard !query.isEmpty else {
return Just([]).eraseToAnyPublisher()
}
isSearching = true
return service.search(query)
.replaceError(with: [])
.eraseToAnyPublisher()
}
}TCA Pattern
swift
import ComposableArchitecture
struct CounterState: Equatable {
var count = 0
}
enum CounterAction: Equatable {
case increment
case decrement
case reset
}
struct CounterEnvironment {
var analytics: AnalyticsService
}
let counterReducer = Reducer<
CounterState,
CounterAction,
CounterEnvironment
> { state, action, environment in
switch action {
case .increment:
state.count += 1
environment.analytics.track("counter_increment")
return .none
case .decrement:
state.count -= 1
environment.analytics.track("counter_decrement")
return .none
case .reset:
state.count = 0
environment.analytics.track("counter_reset")
return .none
}
}
struct CounterView: View {
let store: Store<CounterState, CounterAction>
var body: some View {
WithViewStore(store) { viewStore in
HStack {
Button("-") {
viewStore.send(.decrement)
}
Text("\(viewStore.count)")
Button("+") {
viewStore.send(.increment)
}
Button("Reset") {
viewStore.send(.reset)
}
}
}
}
}Persistence
State Restoration
swift
class PersistentState: ObservableObject {
@Published var items: [Item] {
didSet {
saveItems()
}
}
private let saveKey = "saved_items"
init() {
self.items = loadItems()
}
private func loadItems() -> [Item] {
guard let data = UserDefaults.standard.data(forKey: saveKey),
let items = try? JSONDecoder().decode([Item].self, from: data) else {
return []
}
return items
}
private func saveItems() {
guard let data = try? JSONEncoder().encode(items) else { return }
UserDefaults.standard.set(data, forKey: saveKey)
}
func addItem(_ item: Item) {
items.append(item)
}
func removeItem(at index: Int) {
items.remove(at: index)
}
}Best Practices
- 1Single Source: Keep single source of truth
- 2Immutable Data: Use immutable state when possible
- 3Derived State: Compute derived state
- 4Isolation: Isolate state concerns
- 5Testing: Test state management
- 6Performance: Optimize state updates
- 7Simplicity: Keep it simple
- 8Documentation: Document state flow
Master state management for better apps!