iOS UIKit中MVVM架构下两个ViewModel的连接方式是否合理?
问题分析与优化方案
你当前将SearchViewModel嵌套进WeatherViewModel的做法并非最佳实践,核心问题在于ViewModel职责边界模糊、耦合度偏高,会给后续维护和复用带来麻烦,具体问题包括:
当前实现的问题
- 职责混乱:
WeatherViewModel本应专注于天气数据的展示逻辑(比如格式化温度、日期),现在却持有并依赖SearchViewModel,导致两个ViewModel的职责交叉,SearchViewModel的搜索逻辑无法单独复用(比如其他页面需要城市搜索功能时,没法直接复用这个ViewModel)。 - 耦合过紧:两个ViewModel绑定在一起,修改其中一方的逻辑(比如
SearchViewModel的数据流类型变更)会直接影响另一方,增加了维护成本。 - 测试困难:测试
WeatherViewModel时必须初始化SearchViewModel,还要模拟它的API请求流程,大幅提升了测试复杂度。
优化方案:明确职责+单向数据流
我们可以通过上层协调+单向数据流的方式,让两个ViewModel各自独立,职责清晰:
SearchViewModel:仅负责城市搜索、API请求、数据组装逻辑,对外暴露数据更新的订阅点(你的Bindable就很合适)。WeatherViewModel:仅负责接收天气数据、处理展示相关的逻辑(比如单位转换、格式化),对外提供视图层需要绑定的数据。- 上层协调:由ViewController(或其他上层组件)来管理两个ViewModel的生命周期,订阅
SearchViewModel的数据流,再将数据传递给WeatherViewModel。
修改后的代码示例
保持SearchViewModel的职责单一
class SearchViewModel { var bindableSearchWeather = Bindable<OverallWeatherModel>() var searchCity: String? { didSet { fetchCityData(city: searchCity ?? "") } } private var overallWeatherModel = OverallWeatherModel() private func fetchCityData(city: String) { Service.shared.fetchCityData(city: city) { [weak self] cityGroup in guard let city = cityGroup.first else { return } self?.overallWeatherModel.city = city.name self?.overallWeatherModel.country = city.country self?.fetchWeatherData(lat: city.lat, long: city.lon) } } private func fetchWeatherData(lat: Double, long: Double) { Service.shared.fetchTempData(lat: lat, long: long) { [weak self] weatherGroup in guard let self = self else { return } self.overallWeatherModel.date = DateFormat.shared.currentDate() self.overallWeatherModel.temp = Int(weatherGroup.main.temp - 273) let weatherIcon = "http://openweathermap.org/img/w/\(weatherGroup.weather?.first?.icon ?? "").png" self.overallWeatherModel.weatherIconUrl = URL(string: weatherIcon) self.overallWeatherModel.windStatus = Int(weatherGroup.wind.speed) self.overallWeatherModel.humidity = weatherGroup.main.humidity self.overallWeatherModel.visibility = weatherGroup.visibility self.overallWeatherModel.airPressure = weatherGroup.main.pressure self.fetchNextWeekData(lat: lat, long: long) } } private func fetchNextWeekData(lat: Double, long: Double) { Service.shared.fetchTempDataForWeek(lat: lat, long: long) { [weak self] nextweek in guard let self = self else { return } self.overallWeatherModel.temp1 = Int(nextweek.list[0].main.temp - 273) self.overallWeatherModel.temp2 = Int(nextweek.list[1].main.temp - 273) self.overallWeatherModel.temp3 = Int(nextweek.list[2].main.temp - 273) self.overallWeatherModel.temp4 = Int(nextweek.list[3].main.temp - 273) self.overallWeatherModel.temp5 = Int(nextweek.list[4].main.temp - 273) let iconBaseUrl = "http://openweathermap.org/img/w/" self.overallWeatherModel.icon1 = URL(string: "\(iconBaseUrl)\(nextweek.list[0].weather.first?.icon ?? "").png") self.overallWeatherModel.icon2 = URL(string: "\(iconBaseUrl)\(nextweek.list[1].weather.first?.icon ?? "").png") self.overallWeatherModel.icon3 = URL(string: "\(iconBaseUrl)\(nextweek.list[2].weather.first?.icon ?? "").png") self.overallWeatherModel.icon4 = URL(string: "\(iconBaseUrl)\(nextweek.list[3].weather.first?.icon ?? "").png") self.overallWeatherModel.icon5 = URL(string: "\(iconBaseUrl)\(nextweek.list[4].weather.first?.icon ?? "").png") self.overallWeatherModel.date1 = DateFormat.shared.nextDate(day: 1) self.overallWeatherModel.date2 = DateFormat.shared.nextDate(day: 2) self.overallWeatherModel.date3 = DateFormat.shared.nextDate(day: 3) self.overallWeatherModel.date4 = DateFormat.shared.nextDate(day: 4) self.overallWeatherModel.date5 = DateFormat.shared.nextDate(day: 5) self.bindableSearchWeather.value = self.overallWeatherModel } } }
调整WeatherViewModel专注于展示逻辑
class WeatherViewModel { var bindableWeather = Bindable<OverallWeatherModel>() // 接收外部传入的天气数据,可在此添加展示相关的处理逻辑 func updateWeatherData(_ weather: OverallWeatherModel) { // 示例:如果需要统一温度格式、日期格式化等,可在此处理 bindableWeather.value = weather } }
在ViewController中协调两者
class WeatherViewController: UIViewController { private let weatherViewModel = WeatherViewModel() private let searchViewModel = SearchViewModel() override func viewDidLoad() { super.viewDidLoad() setupDataBindings() } private func setupDataBindings() { // 订阅SearchViewModel的搜索结果,传递给WeatherViewModel searchViewModel.bindableSearchWeather.bind { [weak self] weather in guard let weather = weather else { return } self?.weatherViewModel.updateWeatherData(weather) } // 绑定WeatherViewModel的数据到视图 weatherViewModel.bindableWeather.bind { [weak self] weather in guard let weather = weather else { return } // 这里写更新UI的逻辑,比如更新温度、城市名称等 } } // 搜索按钮点击事件示例 @IBAction func searchButtonTapped(_ sender: UIButton) { // 假设这里获取输入框的城市名称 searchViewModel.searchCity = "Shanghai" } }
优化后的优势
- 职责单一:每个ViewModel只做一件事,逻辑清晰易维护。
- 高复用性:
SearchViewModel可以直接复用在其他需要城市搜索功能的页面。 - 易测试:可以单独测试
SearchViewModel的API请求逻辑,以及WeatherViewModel的展示格式化逻辑,无需依赖对方。
内容的提问来源于stack exchange,提问作者Togosling
相关产品推荐
相关产品推荐

