DEV Community

bright inventions
bright inventions

Posted on • Edited on • Originally published at brightinventions.pl

My motives for MVVM + FlowControllers path

Header

There are many different approaches to building an iOS application. MVC (Model View Controller), MVP (Model View Presenter), MVVM (Model View ViewModel), VIPER (View Interactor Presenter Entity Routing), Redux... And surely, you can name many others. I've been writing iOS apps for some time now, and I have noticed that MVVM + FlowControllers approach works pretty well for me.
In this post I'd like to focus on "Why?" I use it over other approaches and "How?" I use it in my apps.

Why?

The beginning

Starting iOS development is usually connected with learning basic patterns that you can use to solve problems that you will run into while struggling to create your first screens in the app. Most probably the first pattern that will take care of your views and logic behind them will be MVC, which is very popular among iOS developers (but not only them). This is mostly caused by the fact that Apple highly promotes this pattern across the UIKit framework and code examples that you can find in their tutorials.

Is MVC bad?

Yes! It's the worst of all! It causes Massive ViewControllers to appear over your app... Jokes aside...

No, it's not. I was really glad when I've recently read post "Much ado about iOS app architecture" . I cannot say that I fully agree with everything that is said there, but there are parts that I can surely identify with. Why was I glad to read this? Because of this sentence:

"No one is forcing you to implement multiple DataSources in one Controller. To initiate network calls in viewDidLoad. To parse JSONs in UIViewController. To hard-wire Views with Singleton instances."

Many developers blame MVC for its horrible mess and chaos which in fact is created by developers themselves. If you lack discipline, then even VIPER will not help you. Certain patterns may make it easier to structure the code properly and keep it clean, but it's always up to you whether you keep the discipline or not.

Should I use MVC if it is not that bad in the end? If I was a skilled consultant I should probably say "it depends". MVC obviously has its own pros and cons and I bet you can find many great articles that will help you to make up your mind.

Why MVVM?

If MVC is not that bad, then what are my motives for following the MVVM path? Just to name a few of them:

So now, after adopting the principles of MVVM pattern, I'm able to have my passive views (as dumb as possible) and my logic that drives my views which is also separated from the UIKit.

Why not going any further with the division approach that patterns like VIPER target really well? Well... you can do that. I've never used VIPER in a big project before (I'd be glad to hear your opinion on this!), however, I'd say that this kind of patterns could be an easy overkill for a small/medium sized apps.

I feel that MVVM works really well if you want to keep your solutions easy to understand while having an ability to easily test your code and have a nice separation from UIKit-dependant parts of your code.

FlowControllers

What are the motives for using them?

It is not something unusual when inside ViewController "A" you find code that is responsible for transition to screen "B". A problem with this approach is that out of a sudden you create a tight coupling between these two entities and you might find yourself in trouble if you want to split or reuse them in other contexts.

Well... It's always good to ask a question - Is it a problem? If you're working on something simple then using this kind of navigation may be completely fine, however, if you're up to something more complicated - adding a flowController will surely help. Keep in mind that adding a flowController is not a high cost task - I tend to use them even in simple apps as they help me to organize my code better and give me a good look on how navigation in my app works. Adding a flowController to your app will help you with:

  • Keeping your screens separated from each other, which will allow you to modularise and reuse them easily.
  • Controlling flow in parts of your app (You will probably have many different flowControllers)
  • Dependency injection place for your ViewModels

The discipline

A pure fact that you start using MVVM and FlowControllers will not instantly make your code base clean. Guess what? You can still end up with Massive ViewModels! It's up to you whether you keep yourself tight and organize your code well.

How?

MVVM

Ok, so how do I use MVVM? How do I keep my ViewModels clean? How do I use FlowControllers? Jump on board and let's see a quick example that will allow us to see the concept in use.

I have to admit that while I was taking my first step into iOS development, I considered ViewModel to be an object that holds values which would be displayed by a view. At that point I did not see that much value in using ViewModels. What changed my point of view was the approach that you can read about on Microsoft patterns and practices. The core information for me was the fact that the View layer in Microsoft's approach was represented as XAML (quote: "with a limited code-behind that does not contain business logic"). Ok... So does it mean that ViewModel is not only about holding values represented by our views? Can it also contain logic that drives these views? YES! Following this approach on iOS, it encourages you to keep your view layer simple (which in case of MVVM would be both UIView and UIViewController) and move the logic to ViewModel. This is the first step that allows us to reach better testability and move our code far away from UIKit.

Let's go step by step through a quick example of a MVVM pattern used together with FlowController.

Let's start with AppDelegate. What do we have here?

   class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var mainNavigationController: UINavigationController!
    var mainFlowController: MainFlowController!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        mainNavigationController = UINavigationController()

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = mainNavigationController
        window?.makeKeyAndVisible()

        mainFlowController = MainFlowController(rootNavigationController: mainNavigationController)
        mainFlowController.startFlow()

        return true
    }

}
Enter fullscreen mode Exit fullscreen mode

The first thing that is important is that I create my MainFlowController which will be responsible for controlling my application flow. The only dependency that I pass here is my main navigation controller that is the root controller of my main window. I've also seen different approaches that pass UIWindow to a flowController directly, however, if you do not need it, then I would prefer to have this "lighter" object which has a more defined responsibility.

What about the MainFlowController itself?

class MainFlowController {

    private let rootNavigationController: UINavigationController

    private lazy var entryViewModel: EntryViewModel = {
        let fetcher = FakeUserFetcher()
        let viewModel = EntryDefaultViewModel(userFetcher: fetcher)
        viewModel.onUserNameSelected = self.onUserNameSelected
        return viewModel
    }()
    private lazy var entryViewController = EntryViewController(viewModel: entryViewModel)

    init(rootNavigationController: UINavigationController) {
        self.rootNavigationController = rootNavigationController
    }

    func startFlow() {
        rootNavigationController.pushViewController(entryViewController, animated: true)
    }

    func onUserNameSelected(userName: String) {
        print("name: \(userName)");
        // Show next screen using selected user name
    }

}
Enter fullscreen mode Exit fullscreen mode

The core element of this FlowController is surely the startFlow method, which in this case pushes a new ViewController to our navigation stack. This ViewController is created with a ViewModel as a parameter. The viewModel will be very important for us in a while, however, from the FlowController's point of view, we're especially interested in onUserNameSelected closure. This is the output of the ViewModel that our FlowController could be potentially interested in. For example, after selecting a user name we could open the next screen that allows us to select a birthday, surname, favourite pet or simply displays the name in a fancy way. Your FlowController allows you to control the flow of your application and makes the ViewModel unaware of the context that it's used in. ViewModel has its job to do and after it's done our FlowController will be notified about it.

The ViewController

class EntryViewController: UIViewController {

    private let viewModel: EntryViewModel

    private lazy var fetchUserButton: UIButton = {        
        let button = UIButton()
        button.addTarget(self, action: #selector(onFetchUserButtonTapped), for: .touchUpInside)
        // ...setup button
        return button
    }()

    private lazy var selectUserButton: UIButton = {
        let button = UIButton()
        button.addTarget(self, action: #selector(onSelectUserButtonTapped), for: .touchUpInside)
        // ...setup button
        return button
    }()

    private let currentUserLabel = UILabel()

    init(viewModel: EntryViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil) // Layout created programamtically, sorry for that ;(
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        super.loadView()
        setupView()
        setupObservers()
    }

    private func setupObservers() {
        currentUserLabel.reactive.text <~ self.viewModel.userName // bind viewModels output to your UI
    }

    @objc func onFetchUserButtonTapped() {
        viewModel.fetchUser()
    }

    @objc func onSelectUserButtonTapped() {
        viewModel.selectUser()
    }

    private func setupView() {
        // setup constraints etc
    }

}
Enter fullscreen mode Exit fullscreen mode

In my case, ViewController is a part of View layer in MVVM. What happens here is:

  • Binding ViewModel's output to UI components
  • Invoke methods from ViewModel
  • If needed, passing lifecycle methods to ViewModel
  • Layout setup

And last but not least - The ViewModel

protocol EntryViewModel {
    func fetchUser()    
    func selectUser()

    var onUserNameSelected: ((String)->Void)? {get set} // If you're using Reactive frameworks, you can also implement this as a stream
    // var onNextSelected: (()->Void)? // Other callbacks could exist here, they do not need to pass data

    var userName: Property<String?> {get} // ReadOnly property that allows others to observe its changes, but not change the property from outside of viewModel
}

class EntryDefaultViewModel: EntryViewModel {

    private let userFetcher: UserFetcher
    private var mutableUserName: MutableProperty<String?> = MutableProperty(nil)
    lazy var userName: Property<String?> = Property(self.mutableUserName)
    weak var onUserNameSelected: ((String)->Void)?

    // injecting dependencies to your viewModel 
    init(userFetcher: UserFetcher) {
        self.userFetcher = userFetcher
    }

    func selectUser() {
        if let userName = userName.value {
            onUserNameSelected?(userName)
        }
    }

    func fetchUser() {        
        // Bind the result of fetchUser() function to a mutableUserName property
        mutableUserName <~ userFetcher.fetchUser().map { $0.name }
    }

}
Enter fullscreen mode Exit fullscreen mode

The ViewModel's part is the actual beating heart for our View layer. It will allow the View to observe changes in properties it exposes. It will also contain logic for various behaviours. Keep in mind that you do not need to keep the entire logic inside the ViewModel, you can easily extract it and add it as dependency - this is what happens with userFetcher. This allows us to keep our ViewModels cleaner and easily testable as we can mock our dependencies.

Summary

Following the way of MVVM + FlowControllers has helped me a lot in separating/organising my code and making in more testable. If I had to pinpoint three benefits of using this approach that are most valuable for me, then it would be:

  • ViewControllers that can be easily reused in different scenarios
  • Comfortable testing of ViewModels that contain your logic and are not coupled with UIKit
  • Application modules that have their navigation defined in one place

If you have never used a similar approach to your apps, then I'd fully encourage you to try it!

Would you like to learn more about this topic? Check these out:

Improve your iOS Architecture with FlowControllers

Swift By Sundell - "Boy, I have a lot of thoughts on this", with special guest Soroush Khanlou

Coordinators – Soroush Khanlou

Originally published at brightinventions.pl

By Eliasz Sawicki, Software Engineer @ Bright Inventions
Blog, Twitter

This article is cross-posted with author's blog.

Top comments (0)