MVVM + RxSwift

The MVVM architecture in iOS has long been a plativist problem. Compared with the traditional MVC architecture, the core of MVVM lies in the process of bidirectional binding, namely the binding between View and ViewModel. The optimal solution to establish binding relationship is to build it in a responsive way. The original aspect of iOS can be built in the way of KVO + KVC. The disadvantage is that the API is relatively complex and the operation is not convenient. Pure Swift objects need to be marked as Dynamic and the life cycle of KVO needs to be manually managed.

RxSwift belongs to the ReactiveX series, there are multiple language versions, basic coverage of all mainstream programming languages, its focus on asynchronous programming and control of observed data (or events) flow API, behind is Microsoft’s team in the development and maintenance, so high stability. RxSwift is a responsive programming idea, so it works well with the MVVM architecture.

ViewModel + ReactorKit

The most core part of MVVM architecture is undoubtedly ViewModel, which is mainly responsible for module logic processing, state maintenance, etc. The word state is used relatively infrequently in iOS development, unlike React components that rely on state as much. In fact, every interactive control depends on state, which determines how the control is represented, so state is also important in iOS development. In the traditional MVC approach, the Controller is responsible for state management, which is managed by the ViewModel in MVVM.

ReactorKit is a lightweight responsive framework that relies on RxSwift and combines Flux. Flux is an architectural idea proposed by faceBook. Its core concept is one-way flow of data. It is also applicable to iOS, which is mainly manifested as Action and State:

The Action issued by the View is processed by Reactor and then thrown by State and bound to the View, that is, an Action will be issued for each State change. In other words, how the View is displayed is passive. If you want to change its rendering method, you need to issue your own Action.

  • View

    ReactorKit classifies both Controller and View as views and uses them by implementing the View protocol:

    class ReactorViewController: UIViewController.View {... }Copy the code
  • Reactor + State

    A Reactor is a ViewModel. A Reactor is also a protocol that defines the behavior of the ViewModel (code snippet from the network) :

    class ReactorViewModel: Reactor {
    
        /// -action: View distributed event
        enum Action {
           case refreshFollowingStatus(Int)
           case follow(Int)}/// -mutation: transition between Action and State
        enum Mutation {
            case setFollowing(Bool)}/// -state: State manager
        struct State {
            var isFollowing: Bool = false
        }
        
        /// - The status manager is initialized
        let initialState: State = State()}Copy the code
    func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    
      /// - Action sent by View will be responded to here
      case let .refreshFollowingStatus(userID): 
      return UserAPI.isFollowing(userID) // create an API stream
          .map { (isFollowing: Bool) - >Mutation in
        
          /// - sends a Mutation
            return Mutation.setFollowing(isFollowing) 
        }
    
      / / / - in the same way
      case let .follow(userID):
      return UserAPI.follow()
          .map { _ -> Mutation in
            return Mutation.setFollowing(true)}}func reduce(state: State, mutation: Mutation) -> State {
    
      /// - Make a copy of State, since State is the structure of the LET declaration
      var state = state 
      switch mutation {
      
       /// - Map the data associated with Mutation to State
       case let .setFollowing(isFollowing):
       
           /// - Change State
           state.isFollowing = isFollowing 
           
           /// - Returns a new State
           return state 
    }
    Copy the code

    This is the general workflow of the ViewModel: Action -> Mutatuin -> State, and some optional apis, such as Transform (), which can be viewed through the official documentation. As can be seen from the above code, ReactorKit conforms to the idea of Flux programming. Simply speaking, State changes need to be made through Action.

  • Perfect View

    As we all know, the Controller in MVVM needs to hold the ViewModel. Similarly, the View protocol in ReactorKit specifies the type of Reactor to display and provides bind(). This method is used to establish a binding relationship between the View and the ViewModel.

    class ReactorViewController: UIViewController.View { 
      func bind(reactor: ReactorViewModel) {
    
       // the Action emitted by the View is bound to the Action generated by the ViewModel(Reactor)
       refreshButton.rx.tap.map { Reactor.Action.refresh }
          .bindTo(reactor.action)
          .addDisposableTo(self.disposeBag)
    
       /// - Bind the State of the ViewModel(Reactor) to the View and immediately render according to the State
       reactor.state.map{$0.isFollowing }
          .bindTo(followButton.rx.isSelected)
          .addDisposableTo(self.disposeBag)
      }
    }
    Copy the code

    With the help of ReactorKit, the ViewModel’s behavior becomes clearer.

Coordinator

Coordinator Navigation layer. In traditional development mode, the jump between pages is through the push() method of navigationController. This method is convenient, but there is coupling between pages to realize the jump. Coordinators are born to solve this problem. Of course, routing or the introduction of intermediate management can also achieve decoupling, but Coordintaor is lighter.

After a Coordinator is imported, the jump logic is invisible to the page and managed by a Coordinator. The Coordinator provides the interface of the navigationController and holds the Controller, and the jump logic is hidden in the Coordinator. Coordinator is an additional layer independent of the MVVM and does not depend on any part of the MVVM. A Coordinator is an interface exposed to each component. Although a page can interact with a Coordinator only through a Coordinator, it also relies on RxSwift.

  • To realize the Coordinator
/// - Event triggered after page Pop
enum PopResult {
    case reload
    case cancel
}

final class ExampleCoordinator: BaseCoordinator<PopResult> {

    override func start(a) -> Observable<PopResult> {
        let controller = ReactorViewController(a)let reactor = ReactorViewModel(a)let cancel = controller.popedAction
            .asObserver()
            .map { PopResult.cancel }

        let delete = reactor.state
            .map{$0.isDelete }
            .filter { (bool) -> Bool in
                return bool
            }
            .map { _ in PopResult.reload }
            
        return Observable
            .merge(cancel, delete)
            .take(1).do(onNext: { [weak self] (result) in
                if let `self` = self {
                    switch result {
                    case .reload:
                        self.navigationController.popViewController(animated: true)
                        break
                    default:
                        break}})}}Copy the code
  • Coordinator Interaction (Page hopping)
self.coordinate(to: ExampleCoordinator())
    .subscribe(onNext: { result in
       switch result {
           case reload:
           ...
           case cancel:
           ...
       }
     })
    .disposed(by: self.disposeBag)
Copy the code
  • Coordinator source code is also simple

    public protocol CoordinatorType: NSObjectProtocol {
       var identifier: UUID { get }
       var childCoordinators: [UUID: Any] { get set }
       var navigationController: RTRootNavigationController! { get set}}public extension CoordinatorType {
    
    func store<T>(coordinator: BaseCoordinator<T>) {
        coordinator.navigationController = navigationController
        childCoordinators[coordinator.identifier] = coordinator
    }
    
    func free<T>(coordinator: BaseCoordinator<T>) {
        childCoordinators[coordinator.identifier] = nil
    }
    
    @discardableResult
    public func coordinate<T>(to coordinator: BaseCoordinator<T>) -> Observable<T> {
        store(coordinator: coordinator)
        return coordinator.start()
            .do(onNext: { [weak self] _ in
                if let `self` = self {
                    self.free(coordinator: coordinator)
                }
            })
       }
    }
    
    public class BaseCoordinator<ResultType> :NSObject.UINavigationControllerDelegate.CoordinatorType {
    
    /// Typealias which will allows to access a ResultType of the Coordainator by `CoordinatorName.CoordinationResult`.
    typealias CoordinationResult = ResultType
    
    public var navigationController: RTRootNavigationController!
    
    func start(a) -> Observable<ResultType> {
        fatalError("Coordinator Start method should be implemented by subclass.")}func noResultStart(a) {
        fatalError("Coordinator noResultStart method should be implemented by subclass.")}/// UINavigationControllerDelegate
    
    public func navigationController(_ navigationController: UINavigationController,
                                     didShow viewController: UIViewController,
                                     animated: Bool) {
    
        // ensure the view controller is popping
        if let transitionCoordinator = navigationController.transitionCoordinator,
            letfromViewController = transitionCoordinator.viewController(forKey: .from), ! navigationController.viewControllers.contains(fromViewController) {
    
            fromViewController.popedAction.onNext(())
            fromViewController.popedAction.onCompleted()
        }
    }
    
       let disposeBag = DisposeBag(a)public let identifier = UUID(a)public var childCoordinators = [UUID: Any]()
    }
    
    Copy the code

Finished!