@Observable vs ObservableObject: Migrating SwiftUI State to iOS 17+

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: ViewModel becomes a plain let vm: ViewModel (or var if you reassign it) — no wrapper needed at all.
  • Sharing via the environment: @EnvironmentObject var vm: ViewModel becomes @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.