MVVM with Combine Tutorial for iOS | Kodeco, the new raywenderlich.com

In this MVVM with Combine Tutorial, you’ll learn how to get started using the Combine framework along with SwiftUI to build an app using the MVVM pattern


This is a companion discussion topic for the original entry at https://www.kodeco.com/4161005-mvvm-with-combine-tutorial-for-ios

Excellent tutorial! However, I am having an issue with the CurrentWeatherView running the final project in XCode 14.3.1 for iOS 16.4 (minimum deployment target iOS 13) on iPhone 14 Max simulator. In the CurrentWeatherViewModel.refresh() method, data is correctly received and correctly mapped to the CurrentWeatherRowViewModel. The problem is that neither of the .sink() method’s callbacks (receiveCompletion or receiveValue) is called and the dataSource property is never set. The view remains fixed with the message “Loading {city}'s weather…” displayed. The URL is correctly formed and returns the correct data when run in an internet browser. No error message is shown in the console.

Has anyone else experienced this issue? Can anyone suggest a remedy?

I am having the same issue using XCode 14.3.1 for iOS 16.4. Sadly, I do not have a remedy.

I am still in the process of gaining experience with SwiftUI, but during debugging, I noticed that the viewModel of the CurrentWeatherView is being recreated periodically due to a certain behavior of the NavigationLink that I haven’t fully comprehended yet.

When the button to navigate to the CurrentWeatherView is clicked, the instance we receive is not the same as the one where the refresh method was called. The instance where refresh was invoked isn’t being retained anywhere, causing it to be freshly instantiated. Consequently, the observable also gets reinstantiated. This situation results in the CurrentWeatherViewModel that is actually being holded by the view not being notified of the data arriving from the API call.

To tackle this issue, I’ve implemented a solution that might not be optimal but serves its purpose. Instead of repeatedly generating instances of CurrentWeatherViewModel, I maintain one instance within the WeeklyWeatherViewModel. I recreate this instance only when the city changes. When creating the CurrentWeatherView, I utilize the CurrentWeatherViewModel instance stored within the WeeklyWeatherViewModel.

This is the version I am using that is working.

import SwiftUI
import Combine

class WeeklyWeatherViewModel: ObservableObject, Identifiable {
  @Published var city: String = ""
  
  @Published var dataSource: [DailyWeatherRowViewModel] = []
  
  private let weatherFetcher: WeatherFetchable
  
  private var disposables = Set<AnyCancellable>()
  
  private var currentWeatherViewModel: CurrentWeatherViewModel?
  
  init(weatherFetcher: WeatherFetcher,
       scheduler: DispatchQueue = DispatchQueue(label: "WeatherViewModel")) {
    self.weatherFetcher = weatherFetcher
    $city
      .dropFirst(1)
      .removeDuplicates()
      .debounce(for: .seconds(0.5), scheduler: scheduler)
      .sink(receiveValue: fetchWeather(forCity:))
      .store(in: &disposables)
  }
  
  func fetchWeather(forCity city: String) {
    self.currentWeatherViewModel = CurrentWeatherViewModel(city: city, weatherFetcher: weatherFetcher)
    weatherFetcher.weeklyWeatherForecast(forCity: city)
      .map { response in
        response.list.map(DailyWeatherRowViewModel.init)
      }
      .map(Array.removeDuplicates)
      .receive(on: DispatchQueue.main)
      .sink(
        receiveCompletion: { [weak self] value in
          guard let self = self else { return }
          switch value {
          case .failure:
            self.dataSource = []
          case .finished:
            break
          }
          
        },
        receiveValue: { [weak self] forecast in
          guard let self = self else { return }
          self.dataSource = forecast
        })
      .store(in: &disposables)
  }
}

extension WeeklyWeatherViewModel {
  var currentWeatherView: some View {
    if currentWeatherViewModel == nil {
      currentWeatherViewModel = CurrentWeatherViewModel(city: city, weatherFetcher: weatherFetcher)
    }
    return CurrentWeatherView(viewModel: currentWeatherViewModel!)
  }
}

So my issue was different.

When trying to tap “Weather Today”, app was crashed with exec bad access.

In CurrentWeatherView.swfit, line 40, I changed to .onApppear, instead of onAppear{…}.

And no longer having error. No other changes. In case, future readers need help in solving it.

XCode was still showing following errors while tested using XCode 15.2, on physical iPhone 8 with iOS 16.5. But this is not causing app crashing and could be unrelated to the tutorial, so I didn’t resolve it.

**2024-04-21 23:24:39.849613+0700 CombinedWeatherApp[941:119968] Successfully load keyboard extensions**

**2024-04-21 23:24:45.090489+0700 CombinedWeatherApp[941:119968] Metal API Validation Enabled**

**2024-04-21 23:24:45.315882+0700 CombinedWeatherApp[941:119968] [PipelineLibrary] Mapping the pipeline data cache failed, errno 22**