MVVM is a library for who wants to start writing iOS application using MVVM (Model-View-ViewModel), written in Swift.
- Base classes for UIViewController, UIView, UITableView, UICollectionView, UITableViewCell and UICollectionCell.
- Base classes for ViewModel, ListViewModel and CellViewModel
- Base classes for UIWekit. Support handle navigation, evaluateJavaScript, handle java script function...
- Services injection: Network service base on Alamofire, Moya library. Localize service, Alert Service, Reachability service, Mail and Share service...
- Custom transitioning for UINavigationController and UIViewController
- Integration Fastlane app distribution.
- iOS 10.0+
- Xcode 10.0+
- Swift 5.0+
The library heavily depends on RxSwift for data-binding and events. For who does not familiar with Reactive Programming, I suggest to start reading about it first. Beside that, here are the list of dependencies:
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
$ gem install cocoapods
To integrate MVVM into your Xcode project using CocoaPods, specify it in your Podfile
:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
pod 'MVVM', :path => '[Provide your path to MVVM.podspec file]'
pod 'SwiftyJSON'
end
Then, run the following command:
$ pod install
-
The library is mainly written using Generic, so please familiar yourself with Swift Generic, and very important point, we can’t use Generic UIViewController to associate with UIViewController on InterfaceBuilder or Storyboard. So programmatically is prefer, but we can still use XIBs to instantiate our view (check example for more details). (Note: In this project Base classes supports both generic and non-generic types)
-
The idea is that each Page will contain a ViewModel property with type to be determined by generic VM or BaseViewModel
I prefer Page or BasePage over ViewController in term of MVVM. For create new a UIViewController please instance of Page<[VM]> class or BasePage class.
UIViewController
open class Page<VM: IViewModel>: UIViewController, IView, ITransionView
In case you do not want to use the Generic type create instance of BasePage class.
///Non-generic type
open class BasePage: UIViewController, ITransitionView
UITableView
open class ListPage<VM: IListViewModel>: Page<VM>
In case you do not want to use the Generic type create instance of BaseListPage class.
///Non-generic type
open class BaseListPage: BasePage, UITableViewDataSource, UITableViewDelegate
UICollectionView
open class CollectionPage<VM: IListViewModel>: Page<VM>
In case you do not want to use the Generic type create instance of BaseCollectionPage class.
///Non-generic type
open class BaseCollectionPage: BasePage
UIWebkit
///Non-generic type
open class BaseWebView: BasePage
Same as Page, View is also a generic UIView, while TableCell and CollectionCell are generic cell that can be used in ListPage and CollectionPage In case you don't want to use generic type you can also use non generic type by create an instance of BaseABC class Ex: BaseView, BaseCollectionCell, BaseTableCell.
open class View<VM: IGenericViewModel>: UIView, IView
///Non-generic type
open class BaseView: UIView, IView
open class CollectionCell<VM: IGenericViewModel>: UICollectionViewCell, IView
///Non-generic type
open class BaseCollectionCell: UICollectionViewCell, IView
open class TableCell<VM: IGenericViewModel>: UITableViewCell, IView
///Non-generic type
open class BaseTableCell: UITableViewCell, IView
- With Generic Type: You must provide VM. They all have generic type VM to determine its own ViewModel
- By inheriting View or Page, and implementing 2 main methods:
open func initialize() {}
open func bindViewAndViewModel() {}
Then we have a full set of a view that can bind with ViewModel
Base classes for our ViewModel binding with: Page, BasePage
open class ViewModel<M: Model>: NSObject, IViewModel
Non-Generic type
open class BaseViewModel: NSObject, IViewModel, IReactable
Base classes for our ListViewModel binding with: ListPage, BaseListPage, CollectionPage, BaseCollectionPage
open class ListViewModel<M: Model, CVM: IGenericViewModel>: ViewModel<M>, IListViewModel
Non-Generic type
open class BaseListViewModel: BaseViewModel, IListViewModel {
Base classes for our CellViewModel binding with: TableCell, BaseTableCell, CollectionCell, BaseCollectionCell
open class CellViewModel<M: Model>: NSObject, IGenericViewModel
Non-Generic type
open class BaseCellViewModel: NSObject, IGenericViewModel, IIndexable, IReactable {
Base classes for our ViewModel only use for instance of BaseWebView.
open class BaseWebViewModel: BaseViewModel
Note: With Generic Type
-
As we can see, ViewModel and CellViewModel use one generic type M (which is based type is Model). This generic type is for us to determine the model type for each ViewModel. The difference between ViewModel and CellViewModel is ViewModel contains navigation service that can help use to navigate between our pages in apllication, while CellViewModel does not.
-
ListViewModel is a bit different. It uses one more generic type CVM, which represented for ViewModel type of a cell in side a page. In the other hand, it contains an items source array that can be bind with a list page or collection page
Please check examples for details usages of these base classes.
To run the example project, clone the repo, and run pod install
from the Example directory first.
$ open MVVMExample.xcworkspace
For example a viewmodel.
import UIKit
import MVVM
import RxCocoa
import RxSwift
import Action
class TimelinePageViewModel: BaseListViewModel {
private let alertService: IAlertService = DependencyManager.shared.getService()
private var networkService: NetworkService?
private var tmpBag: DisposeBag?
let rxTille = BehaviorRelay<String>(value: "")
lazy var getDataAction: Action<Void, Void> = {
return Action { .just(self.getData()) }
}()
lazy var loadMoreAction: Action<Void, Void> = {
return Action { .just(self.loadMore()) }
}()
let rxState = PublishRelay<NetworkServiceState>()
override func react() {
super.react()
guard let model = self.model as? TabbarModel else { return }
rxTille.accept(model.title)
networkService = DependencyManager.shared.getService()
}
private func getData() {
self.networkService?.loadTimeline(withPage: self.page, withLimit: self.limit)
.map(prepareSources).subscribe(onSuccess: {[weak self] (results) in
if let data = results {
self?.itemsSource.append(data, animated: false)
}
self?.rxState.accept(.success)
}, onError: { (error) in
self.rxState.accept(.error)
}) => tmpBag
}
private func loadMore() {
print("Loading more content...")
}
private func prepareSources(_ response: TimelineResponseModel?) -> [BaseCellViewModel]? {
guard let response = response else { return [] }
if response.stat == .badRequest {
alertService.presentOkayAlert(title: "Error",
message: "\(response.message)\nPlease be sure to provide your own SECRET key from [ABC].")
}
return response.timelines as? [BaseCellViewModel]
}
override func selectedItemDidChange(_ cellViewModel: BaseCellViewModel,_ indexPath: IndexPath) {
/// Do something
}
}
And View
import UIKit
import MVVM
import RxSwift
import RxCocoa
class TimelinePage: BaseListPage {
let indicatorView = UIActivityIndicatorView(style: .gray)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
guard let viewModel = self.viewModel as? TimelinePageViewModel else { return }
viewModel.getDataAction.execute()
}
override func initialize() {
super.initialize()
/// Before using. You must register necessary service
DependencyManager.shared.registerService(Factory<NetworkService> {
NetworkService()
})
DependencyManager.shared.registerService(Factory<ShareService> {
ShareService()
})
indicatorView.hidesWhenStopped = true
}
override func bindViewAndViewModel() {
super.bindViewAndViewModel()
guard let viewModel = self.viewModel as? TimelinePageViewModel else { return }
viewModel.rxTille ~> self.rx.title => disposeBag
// Call out load more when reach to end of table view
tableView.rx.endReach(30).subscribe(onNext: {
viewModel.loadMoreAction.execute(())
}) => disposeBag
}
override func setupTableView(_ tableView: UITableView) {
super.setupTableView(tableView)
/// Register table view cell
tableView.register(cellType: ActivityCell.self)
tableView.register(cellType: TimeLineCell.self)
}
override func getItemSource() -> RxCollection? {
/// Provide data source for UITableView
guard let viewModel = viewModel as? TimelinePageViewModel else { return nil }
return viewModel.itemsSource
}
override func cellIdentifier(_ cellViewModel: Any, _ returnClassName: Bool = false) -> String {
switch cellViewModel {
case is ActivityCellViewModel:
return ActivityCell.identifier(returnClassName)
case is TimelineCellViewModel:
return TimeLineCell.identifier(returnClassName)
default:
return TimeLineCell.identifier(returnClassName)
}
}
override func selectedItemDidChange(_ cellViewModel: Any,_ indexPath: IndexPath) {
/// Handle did tap on table view cell
let page = PostDetailPage()
let animator = RectanglerAnimator(withDuration: TimeInterval(0.5), isPresenting: false)
navigationService.push(to: page, options: .push(with: animator))
}
override func destroy() {
super.destroy()
viewModel?.destroy()
}
}
- Testing Please check on testing branch. Excute basic test case with coverage over 80%.
The library also supports services injection (for Unit Test) and some built-in services, especially navigation service, that helps us to navigate between our pages. Navigation service, by default, is injected to Page and ViewModel By calling
DependencyManager.shared.registerDefaults()
to register for all built-in services (NavigationService, StorageService and AlertService) in the library Or you can create your own navigation service and override the default injection. See examples for detail steps to setup services injection.
The library also supports for page transitions, including pages inside a navigation page and pages that presents modally. See examples for how to implement page transitions
MVVM is available under the MIT license. See the LICENSE file for more info.