You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS UIKit中MVVM架构下两个ViewModel的连接方式是否合理?

问题分析与优化方案

你当前将SearchViewModel嵌套进WeatherViewModel的做法并非最佳实践,核心问题在于ViewModel职责边界模糊、耦合度偏高,会给后续维护和复用带来麻烦,具体问题包括:

当前实现的问题

  • 职责混乱:WeatherViewModel本应专注于天气数据的展示逻辑(比如格式化温度、日期),现在却持有并依赖SearchViewModel,导致两个ViewModel的职责交叉,SearchViewModel的搜索逻辑无法单独复用(比如其他页面需要城市搜索功能时,没法直接复用这个ViewModel)。
  • 耦合过紧:两个ViewModel绑定在一起,修改其中一方的逻辑(比如SearchViewModel的数据流类型变更)会直接影响另一方,增加了维护成本。
  • 测试困难:测试WeatherViewModel时必须初始化SearchViewModel,还要模拟它的API请求流程,大幅提升了测试复杂度。

优化方案:明确职责+单向数据流

我们可以通过上层协调+单向数据流的方式,让两个ViewModel各自独立,职责清晰:

  1. SearchViewModel:仅负责城市搜索、API请求、数据组装逻辑,对外暴露数据更新的订阅点(你的Bindable就很合适)。
  2. WeatherViewModel:仅负责接收天气数据、处理展示相关的逻辑(比如单位转换、格式化),对外提供视图层需要绑定的数据。
  3. 上层协调:由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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 06:25:01