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
- Example
- Contributing
- Versioning
- Authors
- License
- Acknowledgments
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
}
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
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.
- Xcode 10+
- Swift 4.2
- Ruby 2.5.1 (rbenv)
rbenv install 2.5.1
gem install bundler
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
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.
For the versions available, see the release on this repository.
We use SemVer for versioning
See also the list of contributors who participated in this project.
This project is licensed under the MIT License - see the LICENSE.md file for details