Skip to content
This repository has been archived by the owner on Oct 15, 2023. It is now read-only.

MVVM-Coordinator + RxSwift and Clean Architecture

License

Notifications You must be signed in to change notification settings

suho/ios-clean-architecture

Repository files navigation

ios-clean-architecture

Build Status

This project contains documents and example of clean architecture of iOS application based on MVVM-C and RxSwift

Table of Contents

MVVM-Coordinator and Clean Architecture

Overview

Clean Architecture

Application

Application is responsible for delivering information to the user and handling user input. This application implemented with MVVM-C. This is place for your Views, ViewModels, Models and Coordinators.

In the current example, Application is implemented with MVVM-C and use RxSwift.

First of all, Model defines Entity and UseCase of the application.

final class Task {
    let id: String
    var name: String
    var startAt: Date
    var createdAt: Date
    var updatedAt: Date
    var isFinish: Bool
}
protocol TaskUseCase {
    func add(_ task: Task) -> Observable<Task>
    func update(_ task: Task) -> Observable<Task>
    func today() -> Observable<[Task]>
    func find(by id: String) -> Observable<Task?>
    func delete(_ task: Task) -> Observable<Void>
}

View contains ViewModel and ViewModel performs pure transformation of a user Input to the Output

protocol View {
    associatedtype ViewModelType: ViewModel
    var viewModel: ViewModelType! { get set }
}

protocol ViewModel {
    associatedtype Input
    associatedtype Output
    associatedtype CoordinatorType: Coordinate

    var coordinator: CoordinatorType? { get set }

    func transform(input: Input) -> Output
}

A Model (Entity, UseCase) can be injected/initializer into a ViewModel and a ViewModel also can be injected into a ViewController (View) via property injection or initializer. This is done by Coordinator.

protocol Coordinate {
    associatedtype Screen
    associatedtype View: UIViewController

    var viewController: View? { get set }
}

Coordinator also contains navigation logic for describing which screens are shown in which order.

// View
final class TodayViewController: ViewController, View {
  var viewModel: TodayViewModel!

  override func bindViewModel() {
        super.bindViewModel()
        // Magic here
    }
}
// ViewModel
final class TodayViewModel: ViewModel {
    struct Input {
        let loadTrigger: Driver<Void>
        let addTrigger: Driver<Void>
        let updateTrigger: Driver<TaskCellViewModel>
        let deleteTrigger: Driver<IndexPath>
    }

    struct Output {
        let taskViewModels: Driver<[TaskCellViewModel]>
        let addTask: Driver<Void>
        let updateTask: Driver<Void>
        let progress: Driver<Float>
        let delete: Driver<Void>
    }

    var coordinator: TodayCoordinator?
    private let useCase: TaskUseCase

    init(useCase: TaskUseCase, coordinator: TodayCoordinator) {
        self.useCase = useCase
        self.coordinator = coordinator
    }

    func transform(input: Input) -> Output {
        // Magic here
    }
}

As I mentioned above, we will implement injections for models, viewmodels, views in Coordinator

final class TabBarCoordinator: Coordinate {

    weak var viewController: TabBarController?

    func showScreen(_ screen: TabBarCoordinator.Screen) {}

    private func todayNavi() -> UINavigationController {
        // View
        let controller = TodayViewController()

        // Init UseCase for inject into ViewModel
        let repository = RealmRepository<Task>()
        let useCase = RealmTask(repository: repository)

        // Coordinator
        let coordinator = TodayCoordinator()
        coordinator.viewController = controller

        // ViewModel
        let viewModel = TodayViewModel(useCase: useCase, coordinator: coordinator)
        controller.viewModel = viewModel

        //
        let navigationController = NavigationController(rootViewController: controller)
        navigationController.tabBarItem = UITabBarItem(title: App.String.today,
                                                       image: App.Image.today,
                                                       tag: 0)
        return navigationController
    }

    // Magic here
}

Service

The Service is a concrete implementation of Model in a specific service. It does hide all implementation details. For example, database implementation whether it is Realm, CoreData, etc.

Sometime, because of framework requirements (e.g. Realm, CoreData), we can't use Model's Entity to implement

final class RTask: Object {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""
    @objc dynamic var startAt: Date = Date()
    @objc dynamic var createdAt: Date = Date()
    @objc dynamic var updatedAt: Date = Date()
    @objc dynamic var isFinish: Bool = false
}

The Service also contains concrete implementations of Model's UseCase, Repositories or Any Services that are defined in Model.

final class RealmTask<R: Repository>: TaskUseCase where R.Entity == Task {
    private let repository: R

    init(repository: R) {
        self.repository = repository
    }

    func add(_ task: Task) -> Observable<Task> {
        return repository.save(task)
    }

    func update(_ task: Task) -> Observable<Task> {
        return repository.save(task)
    }

    // Another magic here
}

After finish implement the Service, we will inject Service~UseCase(Model) into ViewModel by Coordinator

Example

The example application will show how I implemented Clean Architecture with MVVM-C The example application is Task Todo App which uses Realm and Network as a proof of concept that the Application is not dependant on the Service implementation detail.

Prerequisites

  • Xcode 10+
  • Swift 4.2
  • Ruby 2.5.1 (rbenv)
rbenv install 2.5.1
gem install bundler

Installing

After you install ruby-2.5.1 and bundler

Run this command to install cocoapods

bundle install

Then, install dependencies in this project via Cocoapods

bundle exec pod install

Now, run your project with Xcode and see the demo app

Contributing

Contributions are welcome πŸŽ‰πŸŽŠ

When contributing to this repository, please first discuss the change you wish to make via issue before making a change.

You can also opening a PR if you want to fix bugs or improve something.

Versioning

For the versions available, see the release on this repository.

We use SemVer for versioning

Authors

See also the list of contributors who participated in this project.

License

This project is licensed under the MIT License - see the LICENSE.md file for details

Acknowledgments