iOS MVVM Tutorial: Refactoring from MVC | raywenderlich.com

In this iOS tutorial, you’ll learn how to convert an MVC app into MVVM. In addition, you’ll learn about the components and advantages of using MVVM.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/6733535-ios-mvvm-tutorial-refactoring-from-mvc

Thanks for the article.

Just wondering, shouldn’t the services be injected into the ViewModel so that it can also be unit tested?

You are correct. However, this tutorial emphasizes the MVVM approach and has a lot to cover in a limited space, so I didn’t implement that here. In addition to the DarkSkyForecastService, the LocationGeocoder, DateFormatter, and NumberFormatter should are also candidates for injection. As I’m sure you know, dependency injection is another topic for tutorials on this site and elsewhere. If you are interested in learning more about DI, there are a number of tutorials that cover that here on our site that you can find by using the search function at the top of this page. Thanks for reading this article.

Step 1 says: 1. geocoder takes a String input such as Washington DC and converts it to a latitude and longitude that it sends to the weather service.

private let geocoder = LocationGeocoder() but I don’t see any input though…?

@cybermew Good question. The line:

private let geocoder = LocationGeocoder()

is simply declaring the geocoder that will be used when there is an address to geocode. In the WeatherViewController viewDidLoad() function there is this line where the geocoder is asked to geocode a default address:

geocoder.geocode(addressString: defaultAddress) { [weak self] locations in

I hope that answers your question.

That is fair. I mostly use MVC and am looking for a complete MVVM implementation with DI etc that I could use in a project is all.

Thank you for replying.

Thanks, that explains it.

May I know what is @testable import Grados about? I am getting a compile error and the article did not explain that, and I can’t find anything about it online. Thank you again in advance.

edit: stupid question, sorry new to unit testing. I realised I need to build it first, and the line of code basically says elevate the access levels for the stuffs inside the Grados module (which is just a name already configured inside the settings; I just assumed it to be MVVMFromMVC)

@shearos What are you using in your project to perform data binding? Maybe I can suggest some other examples.

@cybernew Glad you figured it out.

This is a fantastic tutorial; thank you.

Would love to potentially see a similar version in future focused around the implementation & best-practices of MVVM specifically in a SwiftUI app.

I know you have a version featuring Combine, but one talking about MVVM holistically in SwiftUI would be a great help, especially given it’s unclear on the extent to which Controllers still exist in that landscape…

Thanks as always for the helpful content.

1 Like

Thank you. As you pointed out, there is a tutorial on MVVM using Combine that does feature SwiftUI. You may also be interested in the book Combine Asynchronous Programming with Swift, which has examples of MVVM using Combine. I’m sure there are many more good sources on the web. And I’m sure that there is more to come.

Can someone explain/elaborate a bit on weak self? in the viewdidload of WeatherViewController why would self be weak in geocoder.geocode(addressString: defaultAddress) { [weak self] locations in ???

Can we use boxing or any other tools for binding INPUT of View Model to UI element. For example, View Model defaultAddress with Textfield.text instead of button?

@happiehappie Happy to answer your question. [weak self] is necessary to avoid a memory leak caused by a retain cycle. The WeatherViewModel has a strong reference to the geocoder. The closure passed to geocoder.geocode contains a reference to the WeatherViewModel because it accesses various properties of the WeatherViewModel. If both references are strong, which is the default, then that memory would never become dereferenced and would never get released. So one of them has to be a weak reference, and that is the child object, or the geocoder. You can find many articles if you search for “Swift memory management retain cycles”.

@tatianakornilova Could you elaborate on your question? I need to better understand what you are asking. Perhaps a snippet of code would help me understand you question so that I can answer it.

I asked, if I have View Model like this:

import Combine
import Foundation

final class TempViewModel: ObservableObject {
    // input
    @Published var city: String = "London"
    // output
    @Published var temp = " "
    
    private var validString:  AnyPublisher<String, Never> {
          $city
               .debounce(for: 0.3, scheduler: RunLoop.main)
               .removeDuplicates()
               .eraseToAnyPublisher()
       }
    
    init() { validString
            .flatMap { (city:String) -> AnyPublisher <String, Never> in
                WeatherAPI.shared.fetchTemperature(for: city)
        }
        .receive(on: RunLoop.main)
        .assign(to: \.temp , on: self)
        .store(in: &self.cancellableSet)
   }
    
    private var cancellableSet: Set<AnyCancellable> = []
}

How can I bind in ViewController bind both @Published properties ( Input and output)?

And I found answer here.

import UIKit
import Combine

class ViewController: UIViewController{
     // MARK: - UI
    @IBOutlet weak var cityTextField: UITextField!{
        didSet {
            cityTextField.isEnabled = true
            cityTextField.becomeFirstResponder()
        }
    }
    
    @IBOutlet weak var temperatureLabel: UILabel!
     // MARK: - View Model
    private let viewModel = TempViewModel()
     
     // MARK: - Life Cycle View Controller
    override func viewDidLoad() {
        super.viewDidLoad()
        cityTextField.text = viewModel.city
        binding()
    }
    
     // MARK: - Combine
    func binding() {
        cityTextField.textPublisher
           .assign(to: \.city, on: viewModel)
           .store(in: &cancellable)
       
        viewModel.$temp
           .sink(receiveValue: {[weak self] temp in
                      self?.temperatureLabel.text = temp})
           .store(in: &cancellable)
    }

     private var cancellable = Set<AnyCancellable>()
}

First we make TextField Publisher:

import UIKit
import Combine

extension UITextField {
    var textPublisher: AnyPublisher<String, Never> {
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: self)
            .compactMap { $0.object as? UITextField } // receiving notifications with objects which are instances of UITextFields
            .map { $0.text ?? "" } // mapping UITextField to extract text
            .eraseToAnyPublisher()
    }
}

And use it in viewDidLoad:

  // MARK: - Life Cycle View Controller
    override func viewDidLoad() {
        super.viewDidLoad()
        cityTextField.text = viewModel.city
        binding()
    }
    
     // MARK: - Combine
    func binding() {
        cityTextField.textPublisher
           .assign(to: \.city, on: viewModel)
           .store(in: &cancellable)
       
        viewModel.$temp
           .sink(receiveValue: {[weak self] temp in
                      self?.temperatureLabel.text = temp})
           .store(in: &cancellable)
    }

     private var cancellable = Set<AnyCancellable>()

I asked you about binding INPUT like UITextField to View Model.

@tatianakornilova I’m glad you were able to find an answer. Thank you for providing more detail. Others will surely benefit.

Hello. Thank you for visiting

From today the Dark Sky API isn’t available anymore to registration. Do you plan to update the tutorial ? Thanks for all your work !

I just found out. I will get an update together ASAP.