← Back to archive

Development · SwiftUI

Object state and ownership in SwiftUI

Compare ObservableObject, @Published, @ObservedObject, and @StateObject, with a focus on ownership and lifetime.

Read the Korean original on Velog ↗English translation

Development archive cover

As a screen grows, keeping its data, logic, and interface in one structure becomes difficult. It matters not only how a view observes an object, but also who creates and owns that object.

Separating responsibilities

Separating code improves collaboration, maintenance, and extensibility. One useful division is the data model, the interface, and the logic that connects them.

// Model
struct FruitModel: Identifiable {
    let id: String = UUID().uuidString
    let name: String
    let count: Int
}

struct ViewModelStudy: View {
    
    @State var fruitArray: [FruitModel] = [
        FruitModel(name: "Apple", count: 5)
    ]
    
    // View
    var body: some View {
        NavigationStack {
            List {
                ForEach(fruitArray) { fruit in
                    HStack {
                        Text("\(fruit.count)")
                            .foregroundColor(.red)
                        Text(fruit.name)
                    }
                }
                .onDelete { indexSet in
                    deleteFruit(index: indexSet)
                }
            }
            .onAppear {
                getFruits()
            }
        }
    }
    
    // View model
    func getFruits() {
        let fruit1 = FruitModel(name: "Banana", count: 7)
        let fruit2 = FruitModel(name: "Carrot", count: 3)
        fruitArray.append(fruit1)
        fruitArray.append(fruit2)
    }
    func deleteFruit(index: IndexSet) {
        fruitArray.remove(atOffsets: index)
    }
}

MVVM architecture Model–View–ViewModel is one way to organize these responsibilities in a SwiftUI application.

Model: the data

Represent application data separately using structures, arrays, and other suitable types.

struct FruitModel: Identifiable {
    let id: String = UUID().uuidString
    let name: String
    let count: Int
}

View model: logic and behavior

A view model can be a class conforming to ObservableObject. It holds the data and behavior the view needs and publishes changes. Properties that should notify observers can use @Published.

@Published Changes to a published property notify observers so SwiftUI can update views that depend on the object.

class FruitViewModel: ObservableObject {
    
    // Observe the data model
    @Published var fruitArray: [FruitModel] = [
        FruitModel(name: "Apple", count: 5)
    ]
    
    init() {
        getFruits()
    }
    
    func getFruits() {
        let fruit1 = FruitModel(name: "Banana", count: 7)
        let fruit2 = FruitModel(name: "Carrot", count: 3)
        fruitArray.append(fruit1)
        fruitArray.append(fruit2)
    }
    func deleteFruit(index: IndexSet) {
        fruitArray.remove(atOffsets: index)
    }
}

View: the interface

The view describes what the user sees and interacts with. It can observe a view model using @ObservedObject, or own one using @StateObject.

struct ViewModelStudy: View {
    
    // Observe the view model
    @ObservedObject var fruitViewModel: FruitViewModel = FruitViewModel()
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(fruitViewModel.fruitArray) { fruit in
                    HStack {
                        Text("\(fruit.count)")
                            .foregroundColor(.red)
                        Text(fruit.name)
                    }
                }
                .onDelete(perform: fruitViewModel.deleteFruit)
            }
        }
    }
}

@ObservedObject versus @StateObject

Both wrappers let SwiftUI observe an object. The important difference is ownership and lifetime, not whether the entire view or only part of it is redrawn.

Aspect@ObservedObject@StateObject
UpdatesObserves published changes and invalidates dependent view content.Observes published changes and invalidates dependent view content.
LifetimeObserves an instance supplied and owned elsewhere. It does not provide persistent ownership for a newly created instance across view updates.SwiftUI creates and retains the instance for the lifetime of the owning view's identity.
Typical useA child view receiving an existing object.A view responsible for creating and owning the object.
struct ViewModelStudy: View {
    
    // The parent owns the model with @StateObject
    @StateObject var fruitViewModel: FruitViewModel = FruitViewModel()
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(fruitViewModel.fruitArray) { fruit in
                    HStack {
                        Text("\(fruit.count)")
                            .foregroundColor(.red)
                        Text(fruit.name)
                    }
                }
                .onDelete(perform: fruitViewModel.deleteFruit)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink {
                    	// Pass the same view model to the child
                        RandomScreen(fruitViewModel: fruitViewModel)
                    } label: {
                        Image(systemName: "arrow.right")
                    }

                }
            }
        }
    }
}

struct RandomScreen: View {
   	
    // The child observes the supplied model
    @ObservedObject var fruitViewModel: FruitViewModel
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        VStack {
            ForEach(fruitViewModel.fruitArray) { fruit in
                Text(fruit.name)
            }
        }
    }
}

Further reading: Apple: StateObject.

← Back to archive