Starting with iOS 17, Apple introduced the @Observable macro as a replacement for the older ObservableObject protocol with @Published properties. It’s not just a syntax change — it fixes a real performance problem and simplifies the property-wrapper story in SwiftUI.
The Old Way: ObservableObject + @Published
class UserViewModel: ObservableObject {
@Published var name = ""
@Published var email = ""
}
struct ProfileView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
Text(viewModel.name) // recomposes if EITHER name OR email changes
}
}
The problem: ObservableObject sends a single “something changed” signal through objectWillChange whenever any @Published property changes. SwiftUI can’t tell which property changed, so a view reading only name still re-evaluates when email changes elsewhere in the same object.
The New Way: @Observable
@Observable
class UserViewModel {
var name = ""
var email = ""
}
struct ProfileView: View {
@State private var viewModel = UserViewModel()
var body: some View {
Text(viewModel.name) // only recomposes when name specifically changes
}
}
Three changes worth noticing: no more @Published on individual properties (the macro tracks all of them), and the view now holds the model with plain @State instead of @StateObject — @Observable types work directly with @State, which is one less property wrapper to choose between.
Property-Wrapper Changes When You Migrate
- Owning the model:
@StateObject var vm = ViewModel()becomes@State var vm = ViewModel(). - Receiving the model from a parent:
@ObservedObject var vm: ViewModelbecomes a plainlet vm: ViewModel(orvarif you reassign it) — no wrapper needed at all. - Sharing via the environment:
@EnvironmentObject var vm: ViewModelbecomes@Environment(ViewModel.self) var vm.
Should You Migrate Existing Code?
@Observable requires a minimum deployment target of iOS 17 (or macOS 14 / watchOS 10 / tvOS 17). If your app still supports iOS 16 or earlier, you can’t adopt it yet. If your minimum target is already 17+, migrating gives you real recomposition savings for free on any view that only reads some of a model’s properties — it’s generally worth doing, but there’s no urgency to rewrite working ObservableObject code that isn’t causing a measured performance problem.