LoginSignup
26
20

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-09-11

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

ReactiveX/RxSwift

GitHubSignupViewController2.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


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

HeroesListViewController.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

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

TaskEditViewController.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


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()
    }

}

26
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
20