如何在Swift/SwiftUI中监听一个ViewModel的数据变化并通知另一个?
跨ViewModel状态同步:选中房屋后自动更新房间列表
你之前用UserDefaults配合viewDidAppear的方式确实不够规范,还可能带来不必要的副作用。要实现选中房屋后自动更新房间列表的效果,核心是让两个子视图共享同一个HousePickerViewModel实例,再通过SwiftUI的响应式机制触发房间加载逻辑。
以下是两种推荐的实现方案:
方案一:View层通过onChange监听选中状态变化
这种方式简单直接,适合逻辑不复杂的场景:
修改后的完整代码
import SwiftUI struct HomeView: View { // 将HousePickerViewModel提升到父视图,让两个子视图共享 @StateObject private var housePickerVM = HousePickerViewModel() var body: some View { TabView { RoomsView(housePickerVM: housePickerVM) .tabItem { Label("Rooms", systemImage: "pencil") } HousePickerView(housePickerVM: housePickerVM) .tabItem { Label("House Picker", systemImage: "magnifyingglass") } } } } struct RoomsView: View { @StateObject private var roomsVM = RoomsViewModel() // 接收共享的HousePickerViewModel实例 @ObservedObject var housePickerVM: HousePickerViewModel var body: some View { NavigationStack { VStack { Text("Rooms:") Text(roomsVM.roomsBasedOnHouse) } // 监听selectedHouse的变化,触发房间加载 .onChange(of: housePickerVM.selectedHouse) { newHouse in if let house = newHouse { roomsVM.loadRoomsForHouse(house: house) } } } } } struct HousePickerView: View { // 接收共享的HousePickerViewModel实例 @ObservedObject var housePickerVM: HousePickerViewModel var body: some View { NavigationStack { List(housePickerVM.houses) { house in HStack { if house.id == housePickerVM.selectedHouse?.id { Image(systemName: "checkmark") } Text(house.name) } .onTapGesture { housePickerVM.pickHouse(house: house) } } } } } class RoomsViewModel: ObservableObject { @Published var roomsBasedOnHouse: String = "" func loadRoomsForHouse(house: House) { if house.name == "Home" { roomsBasedOnHouse = "Living Room, Bedroom, Garage" } else if house.name == "Vacation Home" { roomsBasedOnHouse = "Patio, Pool Room, Bar" } } } class HousePickerViewModel: ObservableObject { @Published var houses = [House](arrayLiteral: House(id: "abc", name: "Home"), House(id: "def", name: "Vacation Home")) @Published var selectedHouse: House? = nil func pickHouse(house: House) { selectedHouse = house } } public struct House: Identifiable { public var id : String public var name: String } struct HomeView_Previews: PreviewProvider { static var previews: some View { HomeView() } }
方案二:ViewModel层通过Combine订阅状态变化
如果想把状态监听逻辑封装在ViewModel内部,更符合MVVM的职责分离,可以用Combine框架实现订阅:
修改后的核心代码
更新RoomsViewModel
import SwiftUI import Combine class RoomsViewModel: ObservableObject { @Published var roomsBasedOnHouse: String = "" private var cancellables = Set<AnyCancellable>() // 订阅HousePickerViewModel的选中状态变化 func subscribeToHouseSelection(_ housePickerVM: HousePickerViewModel) { housePickerVM.$selectedHouse .compactMap { $0 } // 过滤掉nil值 .sink { [weak self] house in self?.loadRoomsForHouse(house: house) } .store(in: &cancellables) } func loadRoomsForHouse(house: House) { if house.name == "Home" { roomsBasedOnHouse = "Living Room, Bedroom, Garage" } else if house.name == "Vacation Home" { roomsBasedOnHouse = "Patio, Pool Room, Bar" } } }
更新RoomsView
struct RoomsView: View { @StateObject private var roomsVM = RoomsViewModel() @ObservedObject var housePickerVM: HousePickerViewModel var body: some View { NavigationStack { VStack { Text("Rooms:") Text(roomsVM.roomsBasedOnHouse) } } .onAppear { // 页面出现时订阅状态变化 roomsVM.subscribeToHouseSelection(housePickerVM) // 初始化时如果已有选中房屋,加载对应房间 if let house = housePickerVM.selectedHouse { roomsVM.loadRoomsForHouse(house: house) } } } }
核心思路说明
- 共享ViewModel实例:把
HousePickerViewModel的创建从子视图提升到父视图HomeView,再传递给两个子视图,确保选中状态在全局是唯一且同步的。 - 响应式触发逻辑:通过SwiftUI的
onChange修饰符或Combine的订阅机制,监听selectedHouse的变化,自动调用loadRoomsForHouse更新房间列表。
内容的提问来源于stack exchange,提问作者Cameron Henige
相关产品推荐
相关产品推荐

