RxSwiftのViewControllerとViewModelのBindingのサンプルコード集

RxSwiftで`ViewController`と`ViewModel`のBinding部分をどう書けばいいかわからなかったので、サンプルコードを集めてみました。

# [ReactiveX/RxSwift](https://github.com/ReactiveX/RxSwift)

[GitHubSignupViewController2.swift](https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample/Examples/GitHubSignup/UsingDriver/GitHubSignupViewController2.swift)

```swift

class GitHubSignupViewController2 : ViewController {
    @IBOutlet weak var usernameOutlet: UITextField!
    @IBOutlet weak var usernameValidationOutlet: UILabel!
    
    @IBOutlet weak var passwordOutlet: UITextField!
    @IBOutlet weak var passwordValidationOutlet: UILabel!
    
    @IBOutlet weak var repeatedPasswordOutlet: UITextField!
    @IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!
    
    @IBOutlet weak var signupOutlet: UIButton!
    @IBOutlet weak var signingUpOulet: UIActivityIndicatorView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewModel = GithubSignupViewModel2(
            input: (
                username: usernameOutlet.rx.text.asDriver(),
                password: passwordOutlet.rx.text.asDriver(),
                repeatedPassword: repeatedPasswordOutlet.rx.text.asDriver(),
                loginTaps: signupOutlet.rx.tap.asDriver()
            ),
            dependency: (
                API: GitHubDefaultAPI.sharedAPI,
                validationService: GitHubDefaultValidationService.sharedValidationService,
                wireframe: DefaultWireframe.sharedInstance
            )
        )

        // bind results to  {
        viewModel.signupEnabled
            .drive(onNext: { [weak self] valid  in
                self?.signupOutlet.isEnabled = valid
                self?.signupOutlet.alpha = valid ? 1.0 : 0.5
            })
            .addDisposableTo(disposeBag)

        viewModel.validatedUsername
            .drive(usernameValidationOutlet.rx.validationResult)
            .addDisposableTo(disposeBag)

        viewModel.validatedPassword
            .drive(passwordValidationOutlet.rx.validationResult)
            .addDisposableTo(disposeBag)

        viewModel.validatedPasswordRepeated
            .drive(repeatedPasswordValidationOutlet.rx.validationResult)
            .addDisposableTo(disposeBag)

        viewModel.signingIn
            .drive(signingUpOulet.rx.animating)
            .addDisposableTo(disposeBag)

        viewModel.signedIn
            .drive(onNext: { signedIn in
                print("User signed in \(signedIn)")
            })
            .addDisposableTo(disposeBag)
        //}

        let tapBackground = UITapGestureRecognizer()
        tapBackground.rx.event
            .subscribe(onNext: { [weak self] _ in
                self?.view.endEditing(true)
            })
            .addDisposableTo(disposeBag)
        view.addGestureRecognizer(tapBackground)
    }
   
    // This is one of the reasons why it's a good idea for disposal to be detached from allocations.
    // If resources weren't disposed before view controller is being deallocated, signup alert view
    // could be presented on top of the wrong screen or could crash your app if it was being presented 
    // while navigation stack is popping.
    
    // This will work well with UINavigationController, but has an assumption that view controller will
    // never be added as a child view controller. If we didn't recreate the dispose bag here,
    // then our resources would never be properly released.
    override func willMove(toParentViewController parent: UIViewController?) {
        if let parent = parent {
            assert(parent as? UINavigationController != nil, "Please read comments")
        }
        else {
            self.disposeBag = DisposeBag()
        }
    }

}
```

[GithubSignupViewModel2.swift](https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample/Examples/GitHubSignup/UsingDriver/GithubSignupViewModel2.swift)

```swift

class GithubSignupViewModel2 {
    // outputs {

    //
    let validatedUsername: Driver<ValidationResult>
    let validatedPassword: Driver<ValidationResult>
    let validatedPasswordRepeated: Driver<ValidationResult>

    // Is signup button enabled
    let signupEnabled: Driver<Bool>

    // Has user signed in
    let signedIn: Driver<Bool>

    // Is signing process in progress
    let signingIn: Driver<Bool>

    // }

    init(
        input: (
            username: Driver<String>,
            password: Driver<String>,
            repeatedPassword: Driver<String>,
            loginTaps: Driver<Void>
        ),
        dependency: (
            API: GitHubAPI,
            validationService: GitHubValidationService,
            wireframe: Wireframe
        )
    ) {
        let API = dependency.API
        let validationService = dependency.validationService
        let wireframe = dependency.wireframe

        /**
         Notice how no subscribe call is being made.
         Everything is just a definition.
         Pure transformation of input sequences to output sequences.

         When using `Driver`, underlying observable sequence elements are shared because
         driver automagically adds "shareReplay(1)" under the hood.

             .observeOn(MainScheduler.instance)
             .catchErrorJustReturn(.Failed(message: "Error contacting server"))

         ... are squashed into single `.asDriver(onErrorJustReturn: .Failed(message: "Error contacting server"))`
        */

        validatedUsername = input.username
            .flatMapLatest { username in
                return validationService.validateUsername(username)
                    .asDriver(onErrorJustReturn: .failed(message: "Error contacting server"))
            }

        validatedPassword = input.password
            .map { password in
                return validationService.validatePassword(password)
            }

        validatedPasswordRepeated = Driver.combineLatest(input.password, input.repeatedPassword, resultSelector: validationService.validateRepeatedPassword)

        let signingIn = ActivityIndicator()
        self.signingIn = signingIn.asDriver()

        let usernameAndPassword = Driver.combineLatest(input.username, input.password) { ($0, $1) }

        signedIn = input.loginTaps.withLatestFrom(usernameAndPassword)
            .flatMapLatest { (username, password) in
                return API.signup(username, password: password)
                    .trackActivity(signingIn)
                    .asDriver(onErrorJustReturn: false)
            }
            .flatMapLatest { loggedIn -> Driver<Bool> in
                let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"
                return wireframe.promptFor(message, cancelAction: "OK", actions: [])
                    // propagate original value
                    .map { _ in
                        loggedIn
                    }
                    .asDriver(onErrorJustReturn: false)
            }


        signupEnabled = Driver.combineLatest(
            validatedUsername,
            validatedPassword,
            validatedPasswordRepeated,
            signingIn
        )   { username, password, repeatPassword, signingIn in
                username.isValid &&
                password.isValid &&
                repeatPassword.isValid &&
                !signingIn
            }
            .distinctUntilChanged()
    }
}
```

# [sergdort/RxMarvel](https://github.com/sergdort/RxMarvel)

[HeroesListViewController.swift](https://github.com/sergdort/RxMarvel/blob/master/FunctionalMarvel/ViewControllers/HeroesListViewController.swift)

```swift

class HeroesListViewController: RxTableViewController {
   lazy var searchDataSource = RxTableViewSectionedReloadDataSource<HeroCellSection>()
   lazy var dataSource = RxTableViewSectionedReloadDataSource<HeroCellSection>()
   lazy var searchContentController = UITableViewController()
   lazy var searchCotroller: UISearchController = {
      return UISearchController(searchResultsController: self.searchContentController)
   }()
   
   @IBOutlet var rightBarButton: UIBarButtonItem!
   
   override func viewDidLoad() {
      super.viewDidLoad()
      setupDataSource()
      setupBindings()
   }
   
}

//   MARK:Private

extension HeroesListViewController {
   
   private func setupBindings() {
      tableView.tableHeaderView = searchCotroller.searchBar
      let input = HeroListViewModel.Input(searchQuery: searchCotroller.searchBar.rx_text.asObservable(),
                                          nextPageTrigger: tableView.rx_nextPageTriger,
                                          searchNextPageTrigger: searchContentController.tableView.rx_nextPageTriger,
                                          dismissTrigger: rightBarButton.rx_tap.asDriver())
    let viewModel = HeroListViewModel(input: input,
                                        api: DefaultHeroAPI())
    
      viewModel.mainTableItems
         .drive(tableView.rx_itemsWithDataSource(dataSource))
         .addDisposableTo(disposableBag)
      
      viewModel.searchTableItems
         .drive(searchContentController.tableView.rx_itemsWithDataSource(searchDataSource))
         .addDisposableTo(disposableBag)
      
      viewModel.dismissTrigger.asDriver(onErrorJustReturn: ())
         .driveNext { [weak self] in
            self?.dismissViewControllerAnimated(true, completion: nil)
         }
         .addDisposableTo(disposableBag)
   }
   
   private func setupDataSource() {
      dataSource.configureCell = BindableCellFactory<HeroListTableViewCell, HeroCellData>.configureCellFromNib
      searchDataSource.configureCell = BindableCellFactory<HeroListTableViewCell, HeroCellData>.configureCellFromNib
      tableView.dataSource = nil
      searchContentController.tableView.dataSource = nil
   }
   
}
```

[HeroListViewModel.swift](https://github.com/sergdort/RxMarvel/blob/master/FunctionalMarvel/ViewModels/HeroListViewModel.swift)

```swift
class HeroListViewModel {
    struct Input {
        let searchQuery: Observable<String>
        let nextPageTrigger: Observable<Void>
        let searchNextPageTrigger: Observable<Void>
        let dismissTrigger: Driver<Void>
    }
   let mainTableItems: Driver<[HeroCellSection]>
   let searchTableItems: Driver<[HeroCellSection]>
   let dismissTrigger: Driver<Void>
   
    init(input: Input, api: HeroAPI, scheduler: SchedulerType = MainScheduler.instance) {
    
    
    searchTableItems = input.searchQuery
        .filter { !$0.isEmpty }.debug("filter")
        .throttle(0.3, scheduler: scheduler)//2
        .debug("throttle")
        .flatMapLatest { //3
            return api.searchItems($0,
                batch: Batch.initial,
                endPoint: EndPoint.Characters,
                nextBatchTrigger: input.searchNextPageTrigger)
               .catchError { _ in
                  return Observable.empty()
               }
        }
        .map { //4
            return $0.map(HeroCellData.init)
        }
        .map {//5
            return [HeroCellSection(items: $0)]
        }
        .asDriver(onErrorJustReturn: [])
    

      mainTableItems = api
        .paginateItems(Batch.initial,
            endPoint: EndPoint.Characters,
            nextBatchTrigger: input.nextPageTrigger)
        .map {
            return [HeroCellSection(items: $0.map(HeroCellData.init))]
        }
        .asDriver(onErrorJustReturn: [])

    
      dismissTrigger = input.dismissTrigger
   }
}
```

# [devxoul/RxTodo](https://github.com/devxoul/RxTodo)

[TaskEditViewController.swift](https://github.com/devxoul/RxTodo/blob/master/RxTodo/Sources/ViewControllers/TaskEditViewController.swift)

```swift
final class TaskEditViewController: BaseViewController {

    // MARK: Constants

    struct Metric {
        static let padding = 15.f
        static let titleInputCornerRadius = 5.f
        static let titleInputBorderWidth = 1 / UIScreen.mainScreen().scale
    }

    struct Font {
        static let titleLabel = UIFont.systemFontOfSize(14)
    }

    struct Color {
        static let titleInputBorder = UIColor.lightGrayColor()
    }


    // MARK: Properties

    let cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Cancel, target: nil, action: Selector())
    let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Done, target: nil, action: Selector())
    let titleInput = UITextField().then {
        $0.autocorrectionType = .No
        $0.font = Font.titleLabel
        $0.layer.cornerRadius = Metric.titleInputCornerRadius
        $0.layer.borderWidth = Metric.titleInputBorderWidth
        $0.layer.borderColor = Color.titleInputBorder.CGColor
    }


    // MARK: Initializing

    init(viewModel: TaskEditViewModelType) {
        super.init()
        self.navigationItem.leftBarButtonItem = self.cancelBarButtonItem
        self.navigationItem.rightBarButtonItem = self.doneBarButtonItem
        self.configure(viewModel)
    }
    
    required convenience init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    // MARK: View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .whiteColor()
        self.view.addSubview(self.titleInput)
    }

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        self.titleInput.becomeFirstResponder()
    }

    override func setupConstraints() {
        self.titleInput.snp_makeConstraints { make in
            make.top.equalTo(20 + 44 + Metric.padding)
            make.left.equalTo(Metric.padding)
            make.right.equalTo(-Metric.padding)
        }
    }


    // MARK: Configuring

    private func configure(viewModel: TaskEditViewModelType) {
        // 2-Way Binding
        (self.titleInput.rx_text <-> viewModel.title)
            .addDisposableTo(self.disposeBag)

        // Input
        self.cancelBarButtonItem.rx_tap
            .bindTo(viewModel.cancelButtonDidTap)
            .addDisposableTo(self.disposeBag)

        self.doneBarButtonItem.rx_tap
            .bindTo(viewModel.doneButtonDidTap)
            .addDisposableTo(self.disposeBag)

        // Output
        viewModel.navigationBarTitle
            .drive(self.navigationItem.rx_title)
            .addDisposableTo(self.disposeBag)

        viewModel.doneButtonEnabled
            .drive(self.doneBarButtonItem.rx_enabled)
            .addDisposableTo(self.disposeBag)

        viewModel.presentCancelAlert
            .driveNext { [weak self] title, message, leaveTitle, stayTitle in
                guard let `self` = self else { return }
                let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
                let actions = [
                    UIAlertAction(title: leaveTitle, style: .Destructive) { _ in
                        viewModel.alertLeaveButtonDidTap.onNext()
                    },
                    UIAlertAction(title: stayTitle, style: .Default) { _ in
                        self.titleInput.becomeFirstResponder()
                        viewModel.alertStayButtonDidTap.onNext()
                    }
                ]
                actions.forEach(alertController.addAction)
                self.view.endEditing(true)
                self.presentViewController(alertController, animated: true, completion: nil)
            }
            .addDisposableTo(self.disposeBag)

        viewModel.dismissViewController
            .driveNext { [weak self] in
                self?.view.endEditing(true)
                self?.dismissViewControllerAnimated(true, completion: nil)
            }
            .addDisposableTo(self.disposeBag)
    }

}
```


[TaskEditViewModel.swift](https://github.com/devxoul/RxTodo/blob/master/RxTodo/Sources/ViewControllers/TaskEditViewModel.swift)

```swift

enum TaskEditViewMode {
    case New
    case Edit(Task)
}

protocol TaskEditViewModelType {

    // 2-Way Binding
    var title: Variable<String> { get }

    // Input
    var cancelButtonDidTap: PublishSubject<Void> { get }
    var doneButtonDidTap: PublishSubject<Void> { get }
    var alertLeaveButtonDidTap: PublishSubject<Void> { get }
    var alertStayButtonDidTap: PublishSubject<Void> { get }
    var memo: PublishSubject<String> { get }

    // Output
    var navigationBarTitle: Driver<String?> { get }
    var doneButtonEnabled: Driver<Bool> { get }
    var presentCancelAlert: Driver<(String, String, String, String)> { get }
    var dismissViewController: Driver<Void> { get }

}

struct TaskEditViewModel: TaskEditViewModelType {

    // MARK: 2-Way Binding

    var title: Variable<String>


    // MARK: Input

    let cancelButtonDidTap = PublishSubject<Void>()
    let doneButtonDidTap = PublishSubject<Void>()
    let alertLeaveButtonDidTap = PublishSubject<Void>()
    let alertStayButtonDidTap = PublishSubject<Void>()
    let memo = PublishSubject<String>()


    // MARK: Output

    let navigationBarTitle: Driver<String?>
    let doneButtonEnabled: Driver<Bool>
    let presentCancelAlert: Driver<(String, String, String, String)>
    let dismissViewController: Driver<Void>


    // MARK: Private

    let disposeBag = DisposeBag()


    // MARK: Initializing

    init(mode: TaskEditViewMode) {
        switch mode {
        case .New:
            self.navigationBarTitle = .just("New")
            self.title = Variable("")

        case .Edit(let task):
            self.navigationBarTitle = .just("Edit")
            self.title = Variable(task.title)
        }

        self.doneButtonEnabled = self.title.asDriver()
            .map { !$0.isEmpty }
            .asDriver(onErrorJustReturn: false)
            .startWith(false)
            .distinctUntilChanged()

        let needsPresentCancelAlert = self.cancelButtonDidTap.asDriver()
            .withLatestFrom(self.title.asDriver())
            .map { title -> Bool in
                switch mode {
                case .New: return !title.isEmpty
                case .Edit(let task): return title != task.title
                }
            }

        self.presentCancelAlert = needsPresentCancelAlert
            .filter { $0 }
            .map { _ in
                let title = "Really?"
                let message = "Changes will be lost."
                return (title, message, "Leave", "Stay")
            }

        let didDone = self.doneButtonDidTap.asDriver()
            .withLatestFrom(self.doneButtonEnabled).filter { $0 }
            .map { _ in Void() }

        switch mode {
        case .New:
            didDone
                .withLatestFrom(self.title.asDriver())
                .map { title in
                    Task(title: title)
                }
                .drive(Task.didCreate)
                .addDisposableTo(self.disposeBag)

        case .Edit(let task):
            didDone
                .withLatestFrom(self.title.asDriver())
                .map { title in
                    task.then {
                        $0.title = title
                    }
                }
                .drive(Task.didUpdate)
                .addDisposableTo(self.disposeBag)
        }

        self.dismissViewController = Driver.of(self.alertLeaveButtonDidTap.asDriver(), didDone,
                                               needsPresentCancelAlert.filter { !$0 }.map { _ in Void() }).merge()
    }

}

```