Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MVVMの実装を縛るFrameworkを開発・導入し、チームでばらつきがあった実装を統一する

Taiki Suzuki
September 17, 2019

 MVVMの実装を縛るFrameworkを開発・導入し、チームでばらつきがあった実装を統一する

iOSDC Japan 2019 Reject Conference days1[非公式]
https://iosdc-reject-conference.connpass.com/event/137280/

Unio
https://github.com/cats-oss/Unio

Taiki Suzuki

September 17, 2019
Tweet

More Decks by Taiki Suzuki

Other Decks in Programming

Transcript

  1. marty_suzuki marty-suzuki Taiki Suzuki ΠϯλʔωοτςϨϏہ ʮAbemaTVʯΛ୲౰͢ΔiOSΤϯδ χΞɻ2014೥αΠόʔΤʔδΣϯτ ৽ଔೖࣾɻ ίϛϡχςΟαʔϏεͰαʔόʔα ΠυΛ୲౰ͨ͠ޙɺiOSΤϯδχΞʹ

    స޲͠ϑΝογϣϯ௨ൢαΠτ ʮVILECTʯͷ্ཱͪ͛ɾӡӦʹै ࣄɻͦͷޙ৽ײ֮SNSʮ755ʯͰͷ։ ൃΛܦͯɺ2017೥3݄ΑΓݱ৬ɻ Popular repositories Overview SAHistoryNavigationViewController Swift 1,559 SAHistoryNavigationViewController realizes iOS task manager like UI in UINavigationController. SABlurImageView Swift 523 You can use blur effect and it's animation easily to call only two methods. URLEmbeddedView Swift 549 URLEmbeddedView automatically caches the object that is confirmed the Open Graph Protocol. ReverseExtension Swift 1,446 A UITableView extension that enables cell insertion from the bottom of a table view.
  2. ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories

    = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  3. ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories

    = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ϩδοΫͷ࣮૷
  4. ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories

    = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ֎෦͔Βͷೖྗ
  5. ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories

    = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ೖྗͷϦϨʔ
  6. ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories

    = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ಺෦ঢ়ଶ
  7. ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories

    = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ֎෦΁ͷग़ྗ
  8. ྫ - ViewͱViewModelͷ઀ଓ let viewModel: SearchViewModel let tableView: UITableView let

    searchButton: UIButton let textField: UITextField viewModel.repositories .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self) ) { row, repository, cell in cell.textLabel?.text = repository.name } .disposed(by: disposeBag) searchButton.rx.tap .subscribe(onNext: { viewModel.search() }) .disposed(by: disposeBag) textField.rx.text .subscribe(onNext: { viewModel.setText($0) }) .disposed(by: disposeBag)
  9. ྫ - ViewͱViewModelͷ઀ଓ let viewModel: SearchViewModel let tableView: UITableView let

    searchButton: UIButton let textField: UITextField viewModel.repositories .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self) ) { row, repository, cell in cell.textLabel?.text = repository.name } .disposed(by: disposeBag) searchButton.rx.tap .subscribe(onNext: { viewModel.search() }) .disposed(by: disposeBag) textField.rx.text .subscribe(onNext: { viewModel.setText($0) }) .disposed(by: disposeBag) ग़ྗͷ൓ө
  10. ྫ - ViewͱViewModelͷ઀ଓ let viewModel: SearchViewModel let tableView: UITableView let

    searchButton: UIButton let textField: UITextField viewModel.repositories .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self) ) { row, repository, cell in cell.textLabel?.text = repository.name } .disposed(by: disposeBag) searchButton.rx.tap .subscribe(onNext: { viewModel.search() }) .disposed(by: disposeBag) textField.rx.text .subscribe(onNext: { viewModel.setText($0) }) .disposed(by: disposeBag) ϢʔβʔΞΫγϣϯͷ఻ୡ
  11. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  12. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } JOUJBMJ[FSͷϩʔΧϧม਺ͱͯ͠ ఆٛ͢Δ͜ͱ΋Ͱ͖Δ
  13. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() let _repositories = BehaviorRelay<[Repository]>(value: []) self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  14. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() let _repositories = BehaviorRelay<[Repository]>(value: []) self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } JOUJBMJ[FSͷϩʔΧϧม਺ͱͯ͠ ఆٛ͢Δ͜ͱ΋Ͱ͖Δ
  15. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  16. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } "OZ0CTFSWFSͰఆٛ͢Δ͜ͱ΋Ͱ͖Δ
  17. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let search: AnyObserver<Void> let

    setText: AnyObserver<String?> let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishSubject<Void>() private let _setText = PublishSubject<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() self.search = _search.asObserver() self.setText = _setText.asObserver() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_setText) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } }
  18. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let search: AnyObserver<Void> let

    setText: AnyObserver<String?> let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishSubject<Void>() private let _setText = PublishSubject<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() self.search = _search.asObserver() self.setText = _setText.asObserver() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_setText) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } } "OZ0CTFSWFSͰఆٛ͢Δ͜ͱ΋Ͱ͖Δ
  19. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ let viewModel: SearchViewModel let tableView: UITableView let

    searchButton: UIButton let textField: UITextField viewModel.repositories .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self) ) { row, repository, cell in cell.textLabel?.text = repository.name } .disposed(by: disposeBag) searchButton.rx.tap .bind(to: viewModel.search) .disposed(by: disposeBag) textField.rx.text .bind(to: viewModel.setText) .disposed(by: disposeBag)
  20. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ let viewModel: SearchViewModel let tableView: UITableView let

    searchButton: UIButton let textField: UITextField viewModel.repositories .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self) ) { row, repository, cell in cell.textLabel?.text = repository.name } .disposed(by: disposeBag) searchButton.rx.tap .bind(to: viewModel.search) .disposed(by: disposeBag) textField.rx.text .bind(to: viewModel.setText) .disposed(by: disposeBag) 7JFX.PEFMଆ͕"OZ0CTFSWFSʹ ͳͬͨ͜ͱͰGVODCJOE UP ʹͳΔ
  21. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ class SearchViewModel { let repositories: Observable<[Repository]> private

    let disposeBag = DisposeBag() init(search: Observable<Void>, setText: Observable<String?>) { let apiAction = SearchAPIAction() let _repositories = BehaviorRelay<[Repository]>(value: []) self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) search .withLatestFrom(setText) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } }
  22. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ class SearchViewModel { let repositories: Observable<[Repository]> private

    let disposeBag = DisposeBag() init(search: Observable<Void>, setText: Observable<String?>) { let apiAction = SearchAPIAction() let _repositories = BehaviorRelay<[Repository]>(value: []) self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) search .withLatestFrom(setText) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } } *OJUJBMJ[FSͷҾ਺ͰೖྗΛड͚औΔ
  23. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ let tableView: UITableView let searchButton: UIButton let

    textField: UITextField let viewModel = SearchViewModel( search: searchButton.rx.tap.asObservable(), setText: textField.rx.text.asObservable() ) viewModel.repositories .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self) ) { row, repository, cell in cell.textLabel?.text = repository.name } .disposed(by: disposeBag)
  24. ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ let tableView: UITableView let searchButton: UIButton let

    textField: UITextField let viewModel = SearchViewModel( search: searchButton.rx.tap.asObservable(), setText: textField.rx.text.asObservable() ) viewModel.repositories .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self) ) { row, repository, cell in cell.textLabel?.text = repository.name } .disposed(by: disposeBag) 7JFX.PEFMͷJOJUJBMJ[FSͷҾ਺ Ͱ౉͍ͯ͠ΔͷͰɺCJOEͷ࣮૷͕ 7JFXଆʹ͸ͳ͍
  25. ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  26. ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ಺෦Ͱ͸ೖྗΛड͚෇͚Δ͚ͩͳ͸ͣͳͷʹ GVODBDDFQU @ ͕ݺ΂ͯ͠·͏
  27. ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ೖྗͷϦϨʔ༻ʹಉ͡Α͏ͳఆٛΛ ࠶౓͠ͳ͚Ε͹ͳΒͳ͍
  28. ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private

    let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } } ಛͷఆٛ৔ॴ͕ܾ·͍ͬͯͳ͍ ͷͰΫϥε಺ͷͲ͜ʹͰ΋ఆٛ Ͱ͖ͯ͠·͏
  29. • Input • Output • State • Extra • Logic

    • UnioStream • Input • Output • State • Extra • Logic • UnioStream Unioͷߏ੒ཁૉ
  30. • Input • Output • State • Extra • Logic

    • UnioStream Unioͷߏ੒ཁૉ
  31. Input - Unioͷߏ੒ཁૉ struct Input: InputType { let search =

    PublishRelay<String>() let selectIndex = PublishSubject<Int>() }
  32. Input - Unioͷߏ੒ཁૉ struct Input: InputType { let search =

    PublishRelay<String>() let selectIndex = PublishSubject<Int>() } 7JFX.PEFM΁ͷೖྗΛఆٛ͢Δ৔ॴ
  33. let input: InputWrapper<Input> input.accept("GitHub", for: \.search) Input - Unioͷߏ੒ཁૉ struct

    Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() }
  34. let input: InputWrapper<Input> input.accept("GitHub", for: \.search) Input - Unioͷߏ੒ཁૉ struct

    Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() } 6OJPͰ͸*OQVU8SBQQFSʹ ϥοϓ͞Εͯެ։͞ΕΔ
  35. let input: InputWrapper<Input> input.accept("GitHub", for: \.search) Input - Unioͷߏ੒ཁૉ struct

    Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() } 1VCMJTI3FMBZͰఆٛ͞Ε͍ͯΔ͕ GVODBDDFQU @ ͔࣮͠ߦͰ͖ͳ͍ʂ
  36. InputWrapperͷ࢓૊Έ class InputWrapper<Input: InputType> { private let input: Input func

    accept<T>(_ value: T, for keyPath: KeyPath<Input, PublishRelay<T>>) { input[keyPath: keyPath].accept(value) } } struct Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() } let input: InputWrapper<Input> input.accept("GitHub", for: \.search)
  37. InputWrapperͷ࢓૊Έ class InputWrapper<Input: InputType> { private let input: Input func

    accept<T>(_ value: T, for keyPath: KeyPath<Input, PublishRelay<T>>) { input[keyPath: keyPath].accept(value) } } QSJWBUFͰJOQVU͕಺෦Ͱอ࣋͞Ε͍ͯΔ ͭ·Γɺ*OQVU8SBQQFS֎͔ΒJOQVUࣗମ΍ ͦͷQSPQFSUZʹ௚઀ΞΫηεͰ͖ͳ͍ struct Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() } let input: InputWrapper<Input> input.accept("GitHub", for: \.search)
  38. InputWrapperͷ࢓૊Έ class InputWrapper<Input: InputType> { private let input: Input func

    accept<T>(_ value: T, for keyPath: KeyPath<Input, PublishRelay<T>>) { input[keyPath: keyPath].accept(value) } } *OQVUͰ1VCMJTI3FMBZ5Ͱఆٛ͞Ε͍ͯΔ QSPQFSUZͷ,FZ1BUIΛड͚औΔ struct Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() } let input: InputWrapper<Input> input.accept("GitHub", for: \.search)
  39. ,FZ1BUI͔Β*OQVU಺ͷ1VCMJTI3FMBZ5Λऔಘ͢Δ ͜ͷ··ฦͯ͠͠·͏ͱ1VCMJTI3FMZ5ͳͷͰ GVODBT0CTFSWBCMF ʹ΋ΞΫηεͰ͖ͯ͠·͏ InputWrapperͷ࢓૊Έ class InputWrapper<Input: InputType> { private

    let input: Input func accept<T>(_ value: T, for keyPath: KeyPath<Input, PublishRelay<T>>) { input[keyPath: keyPath].accept(value) } } struct Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() } let input: InputWrapper<Input> input.accept("GitHub", for: \.search)
  40. ೖྗ͢Δ͚ͩͷঢ়ଶΛ࣮ݱ͢ΔͨΊʹ ಺෦ͰGVODBDDFQU @ Λ࣮ߦ͢Δ InputWrapperͷ࢓૊Έ class InputWrapper<Input: InputType> { private

    let input: Input func accept<T>(_ value: T, for keyPath: KeyPath<Input, PublishRelay<T>>) { input[keyPath: keyPath].accept(value) } } struct Input: InputType { let search = PublishRelay<String>() let selectIndex = PublishSubject<Int>() } let input: InputWrapper<Input> input.accept("GitHub", for: \.search)
  41. • Input • Output • State • Extra • Logic

    • UnioStream Unioͷߏ੒ཁૉ
  42. Output - Unioͷߏ੒ཁૉ struct Output: OutputType { let isHidden =

    BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") }
  43. Output - Unioͷߏ੒ཁૉ struct Output: OutputType { let isHidden =

    BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } 7JFX.PEFM΁ͷग़ྗΛఆٛ͢Δ৔ॴ
  44. Output - Unioͷߏ੒ཁૉ let output: OutputWrapper<Output> let label: UILabel label.isHidden

    = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") }
  45. Output - Unioͷߏ੒ཁૉ let output: OutputWrapper<Output> let label: UILabel label.isHidden

    = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } 6OJPͰ͸0VUQVU8SBQQFSʹ ϥοϓ͞Εͯެ։͞ΕΔ
  46. Output - Unioͷߏ੒ཁૉ let output: OutputWrapper<Output> let label: UILabel label.isHidden

    = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } #FIBWJPS3FMBZͰఆٛ͞Ε͍ͯΔ͕WBMVF΍ GVODBT0CTFSWBCMF ͔࣮͠ߦͰ͖ͳ͍ʂ
  47. struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let

    title = BehaviorSubject(value: "title") } let output: OutputWrapper<Output> let label: UILabel label.isHidden = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) class OutputWrapper<Output: OutputType> { private let output: Output func value<T>(for keyPath: KeyPath<Output, BehaviorRelay<T>>) -> T { return output[keyPath: keyPath].value } func observable<T>(for keyPath: KeyPath<Output, BehaviorRelay<T>>) -> Observable<T> { return output[keyPath: keyPath].asObservable() } } OutputWrapperͷ࢓૊Έ
  48. class OutputWrapper<Output: OutputType> { private let output: Output func value<T>(for

    keyPath: KeyPath<Output, BehaviorRelay<T>>) -> T { return output[keyPath: keyPath].value } func observable<T>(for keyPath: KeyPath<Output, BehaviorRelay<T>>) -> Observable<T> { return output[keyPath: keyPath].asObservable() } } QSJWBUFͰPVUQVU͕಺෦Ͱอ࣋͞Ε͍ͯΔ ͭ·Γɺ0VUQVU8SBQQFS֎͔ΒJOQVUࣗମ΍ ͦͷQSPQFSUZʹ௚઀ΞΫηεͰ͖ͳ͍ OutputWrapperͷ࢓૊Έ struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } let output: OutputWrapper<Output> let label: UILabel label.isHidden = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden)
  49. class OutputWrapper<Output: OutputType> { private let output: Output func value<T>(for

    keyPath: KeyPath<Output, BehaviorRelay<T>>) -> T { return output[keyPath: keyPath].value } func observable<T>(for keyPath: KeyPath<Output, BehaviorRelay<T>>) -> Observable<T> { return output[keyPath: keyPath].asObservable() } } OutputWrapperͷ࢓૊Έ struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } let output: OutputWrapper<Output> let label: UILabel label.isHidden = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) 0VUQVUͰ#FIBWJPS3FMBZ5Ͱఆٛ͞Ε͍ͯΔ QSPQFSUZͷ,FZ1BUIΛड͚औΔ
  50. class OutputWrapper<Output: OutputType> { private let output: Output func value<T>(for

    keyPath: KeyPath<Output, BehaviorRelay<T>>) -> T { return output[keyPath: keyPath].value } func observable<T>(for keyPath: KeyPath<Output, BehaviorRelay<T>>) -> Observable<T> { return output[keyPath: keyPath].asObservable() } } OutputWrapperͷ࢓૊Έ struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } let output: OutputWrapper<Output> let label: UILabel label.isHidden = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) ,FZ1BUI͔Β0VUQVU಺ͷ#FIBWJPS3FMBZ5Λऔಘ͢Δ ͜ͷ··ฦͯ͠͠·͏ͱ#FIBWJPS3FMZ5ͳͷͰ GVODBDDFQU @ ʹ΋ΞΫηεͰ͖ͯ͠·͏
  51. class OutputWrapper<Output: OutputType> { private let output: Output func value<T>(for

    keyPath: KeyPath<Output, BehaviorRelay<T>>) -> T { return output[keyPath: keyPath].value } func observable<T>(for keyPath: KeyPath<Output, BehaviorRelay<T>>) -> Observable<T> { return output[keyPath: keyPath].asObservable() } } OutputWrapperͷ࢓૊Έ struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } let output: OutputWrapper<Output> let label: UILabel label.isHidden = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) ஋Λड͚औΔ͚ͩͷঢ়ଶΛ࣮ݱ͢Δ ͨΊʹ಺෦ͰWBMVFΛฦ͢
  52. class OutputWrapper<Output: OutputType> { private let output: Output func value<T>(for

    keyPath: KeyPath<Output, BehaviorRelay<T>>) -> T { return output[keyPath: keyPath].value } func observable<T>(for keyPath: KeyPath<Output, BehaviorRelay<T>>) -> Observable<T> { return output[keyPath: keyPath].asObservable() } } OutputWrapperͷ࢓૊Έ struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } let output: OutputWrapper<Output> let label: UILabel label.isHidden = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden) 0CTFSWBCMFΛड͚औΔ͚ͩͷঢ়ଶΛ࣮ݱ͢ΔͨΊʹ ಺෦ͰGVODBT0CTFSWBCMF ͨ݁͠ՌΛฦ͢
  53. Output - Unioͷߏ੒ཁૉ let output: OutputWrapper<Output> let label: UILabel label.text

    = try? output.value(for: \.title) output.observable(for: \.title).bind(to: label.rx.text) struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") }
  54. Output - Unioͷߏ੒ཁૉ let output: OutputWrapper<Output> let label: UILabel label.text

    = try? output.value(for: \.title) output.observable(for: \.title).bind(to: label.rx.text) struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } #FIBWJSP4VCKFDUͷ৔߹ɺ஋ʹΞΫηε͢Δࡍʹྫ֎Λ ౤͛Δ৔߹͕͋ΔͷͰɺUSZ͢Δ͜ͱͰΞΫηεͰ͖Δ
  55. • Input • Output • State • Extra • Logic

    • UnioStream Unioͷߏ੒ཁૉ
  56. State - Unioͷߏ੒ཁૉ struct State: StateType { let count =

    BehaviorRelay(value: 0) } 7JFX.PEFMͷ಺෦ঢ়ଶΛఆٛ͢Δ৔ॴ
  57. • Input • Output • State • Extra • Logic

    • UnioStream Unioͷߏ੒ཁૉ
  58. Extra - Unioͷߏ੒ཁૉ struct Extra: ExtraType { let userDefaults: UserDefaults

    let notificationCenter: NotificationCenter } 7JFX.PEFMͷ֎෦ґଘΛఆٛ͢Δ৔ॴ
  59. • Input • Output • State • Extra • Logic

    • UnioStream Unioͷߏ੒ཁૉ
  60. Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag

    = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { dependency.inputObservable(for: \.increment) .withLatestFrom(dependency.state.count) { $1 + 1 } .bind(to: dependency.state.count) .disposed(by: disposeBag) return Output(text: dependency.state.count.map(String.init)) } } struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) }
  61. Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag

    = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { dependency.inputObservable(for: \.increment) .withLatestFrom(dependency.state.count) { $1 + 1 } .bind(to: dependency.state.count) .disposed(by: disposeBag) return Output(text: dependency.state.count.map(String.init)) } } struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) } *OQVUɾ4UBUFɾ&YUSBΛར༻ͯ͠ 0VUQVUΛੜ੒Ͱ͖Δ།Ұͷ৔ॴ
  62. Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag

    = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { dependency.inputObservable(for: \.increment) .withLatestFrom(dependency.state.count) { $1 + 1 } .bind(to: dependency.state.count) .disposed(by: disposeBag) return Output(text: dependency.state.count.map(String.init)) } } struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) } *OQVUɾ4UBUFɾ&YUSBʹΞΫηεͰ͖Δ
  63. Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag

    = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { dependency.inputObservable(for: \.increment) .withLatestFrom(dependency.state.count) { $1 + 1 } .bind(to: dependency.state.count) .disposed(by: disposeBag) return Output(text: dependency.state.count.map(String.init)) } } struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) } %FQFOEFODZΛհͯ͠ɺ*OQVUͷ೚ҙͷ1VCMJTI3FMBZʹ ରͯ͠0CTFSWBCMFͱͯ͠ΞΫηε͢Δ͜ͱ͕Ͱ͖Δ ΋ͪΖΜɺGVODBDDFQU @ ͷݺͼग़͠͸Ͱ͖ͳ͍
  64. Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag

    = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { dependency.inputObservable(for: \.increment) .withLatestFrom(dependency.state.count) { $1 + 1 } .bind(to: dependency.state.count) .disposed(by: disposeBag) return Output(text: dependency.state.count.map(String.init)) } } struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) } %FQFOEFODZΛհͯ͠ɺ4UBUFʹΞΫηεͰ͖Δ
  65. Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag

    = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { dependency.inputObservable(for: \.increment) .withLatestFrom(dependency.state.count) { $1 + 1 } .bind(to: dependency.state.count) .disposed(by: disposeBag) return Output(text: dependency.state.count.map(String.init)) } } struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) } *OQVUɾ4UBUFɾ&YUSBΛར༻ͯ͠0VUQVUΛੜ੒
  66. • Input • Output • State • Extra • Logic

    • UnioStream Unioͷߏ੒ཁૉ
  67. UnioStream - Unioͷߏ੒ཁૉ protocol CounterViewStreamType: AnyObject { var input: InputWrapper<CounterViewStream.Input>

    { get } var output: OutputWrapper<CounterViewStream.Output> { get } } final class CounterViewStream: UnioStream<CounterViewStream.Logic>, CounterViewStreamType { struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) } struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { ... return Output(text: dependency.state.count.map(String.init)) } } }
  68. UnioStream - Unioͷߏ੒ཁૉ protocol CounterViewStreamType: AnyObject { var input: InputWrapper<CounterViewStream.Input>

    { get } var output: OutputWrapper<CounterViewStream.Output> { get } } final class CounterViewStream: UnioStream<CounterViewStream.Logic>, CounterViewStreamType { struct Input: InputType { let increment = PublishRelay<Void>() } struct Output: OutputType { let text: Observable<String> } struct State: StateType { let count = BehaviorRelay(value: 0) } struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, NoExtra>) -> Output { ... return Output(text: dependency.state.count.map(String.init)) } } } *OQVUɾ0VUQVUɾ4UBUFɾ&YUSBɾ-PHJDΛ ͭͳ͗߹Θͤͯ୯Ұํ޲ͷೖग़ྗΛ࣮ݱ͢Δ
  69. let stream: CounterViewStreamType let button: UIButton let label: UILabel button.rx.tap

    .bind(to: stream.input.accept(for: \.increment)) .disposed(by: disposeBag) stream.output.observable(for: \.text) .bind(to: label.rx.text) .disposed(by: disposeBag) UnioStream - Unioͷߏ੒ཁૉ
  70. let stream: CounterViewStreamType let button: UIButton let label: UILabel button.rx.tap

    .bind(to: stream.input.accept(for: \.increment)) .disposed(by: disposeBag) stream.output.observable(for: \.text) .bind(to: label.rx.text) .disposed(by: disposeBag) UnioStream - Unioͷߏ੒ཁૉ 6OJP4USFBN͕อ͍࣋ͯ͠Δ*OQVU8SBQQFSΛ հͯ͠ϘλϯͷλοϓΛೖྗͱͯ͠఻ୡ͢Δ
  71. let stream: CounterViewStreamType let button: UIButton let label: UILabel button.rx.tap

    .bind(to: stream.input.accept(for: \.increment)) .disposed(by: disposeBag) stream.output.observable(for: \.text) .bind(to: label.rx.text) .disposed(by: disposeBag) UnioStream - Unioͷߏ੒ཁૉ 6OJP4USFBN͕อ͍࣋ͯ͠Δ0VUQVU8SBQQFSΛ հͯ͠ग़ྗΛϥϕϧʹ൓ө͢Δ
  72. UnioStreamͷ࢓૊Έ open class UnioStream<Logic: LogicType> { public let input: InputWrapper<Logic.Input>

    public let output: OutputWrapper<Logic.Output> private let _state: Logic.State private let _extra: Logic.Extra private let _logic: Logic public init(input: Logic.Input, state: Logic.State, extra: Logic.Extra, logic: Logic) { let dependency = Dependency(input: input, state: state, extra: extra) let output = logic.bind(from: dependency) self.input = InputWrapper(input) self.output = OutputWrapper(output) self._state = state self._extra = extra self._logic = logic } }
  73. UnioStreamͷ࢓૊Έ open class UnioStream<Logic: LogicType> { public let input: InputWrapper<Logic.Input>

    public let output: OutputWrapper<Logic.Output> private let _state: Logic.State private let _extra: Logic.Extra private let _logic: Logic public init(input: Logic.Input, state: Logic.State, extra: Logic.Extra, logic: Logic) { let dependency = Dependency(input: input, state: state, extra: extra) let output = logic.bind(from: dependency) self.input = InputWrapper(input) self.output = OutputWrapper(output) self._state = state self._extra = extra self._logic = logic } } *OQVU8SBQQFSͱ0VUQVU8SBQQFSΛ QSPQFSUZͱͯ͠อ͍࣋ͯ͠Δ
  74. UnioStreamͷ࢓૊Έ open class UnioStream<Logic: LogicType> { public let input: InputWrapper<Logic.Input>

    public let output: OutputWrapper<Logic.Output> private let _state: Logic.State private let _extra: Logic.Extra private let _logic: Logic public init(input: Logic.Input, state: Logic.State, extra: Logic.Extra, logic: Logic) { let dependency = Dependency(input: input, state: state, extra: extra) let output = logic.bind(from: dependency) self.input = InputWrapper(input) self.output = OutputWrapper(output) self._state = state self._extra = extra self._logic = logic } } ॳظԽ࣌ʹ%FQFOEFODZ͕ੜ੒͞Εɺ-PHJDͷ GVODCJOE GSPN ͕౓͚ͩݺ͹ΕΔ ͦͯ͠ɺGVODCJOE GSPN ͔ΒPVUQVU͕ੜ੒͞ΕΔ
  75. UnioStreamͷ࢓૊Έ open class UnioStream<Logic: LogicType> { public let input: InputWrapper<Logic.Input>

    public let output: OutputWrapper<Logic.Output> private let _state: Logic.State private let _extra: Logic.Extra private let _logic: Logic public init(input: Logic.Input, state: Logic.State, extra: Logic.Extra, logic: Logic) { let dependency = Dependency(input: input, state: state, extra: extra) let output = logic.bind(from: dependency) self.input = InputWrapper(input) self.output = OutputWrapper(output) self._state = state self._extra = extra self._logic = logic } } JOQVUͱੜ੒͞ΕͨPVUQVUΛ΋ͱʹ 6OJP4USFBNͷೖग़ྗΛ֬ఆ͢Δ
  76. UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  77. UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  78. UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  79. UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  80. UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  81. UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay<Void>() private let _text = PublishRelay<String?>() private let disposeBag = DisposeBag() init() { let apiAction = SearchAPIAction() self.repositories = _repositories.asObservable() apiAction.response .bind(to: _repositories) .disposed(by: disposeBag) _search .withLatestFrom(_text) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { apiAction.execute($0) }) .disposed(by: disposeBag) } func search() { _search.accept(()) } func setText(_ text: String?) { _text.accept(text) } }
  82. Model΋UnioStreamԽ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } }
  83. Model΋UnioStreamԽ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } }
  84. Model΋UnioStreamԽ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction = SearchAPIAction() } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let extra = dependency.extra extra.apiAction.response .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .subscribe(onNext: { extra.apiAction.execute($0) }) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } protocol SearchAPIActionType { var response: Observable<[Repository]> { get } var error: Observable<Error> { get } func execute(_ text: String) }
  85. Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class SearchAPIStream: UnioStream<SearchAPIStream.Logic>, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay<String>() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable<Error> } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency<Input, NoState, Extra>) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable<Event<[Repository]>> in let request = URLRequest(url: URL(string: "https://◦◦?q=\(query)")!) return extra.session.rx.data(request: request) .take(1) .map { try extra.decoder.decode([Repository].self, from:$0) } .materialize() } .share() return Output(response: result.flatMap { $0.element.map(Observable.just) ?? .empty() }, error: result.flatMap { $0.error.map(Observable.just) ?? .empty() }) } } } protocol SearchAPIActionType { var response: Observable<[Repository]> { get } var error: Observable<Error> { get } func execute(_ text: String) }
  86. Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class SearchAPIStream: UnioStream<SearchAPIStream.Logic>, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay<String>() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable<Error> } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency<Input, NoState, Extra>) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable<Event<[Repository]>> in let request = URLRequest(url: URL(string: "https://◦◦?q=\(query)")!) return extra.session.rx.data(request: request) .take(1) .map { try extra.decoder.decode([Repository].self, from:$0) } .materialize() } .share() return Output(response: result.flatMap { $0.element.map(Observable.just) ?? .empty() }, error: result.flatMap { $0.error.map(Observable.just) ?? .empty() }) } } } protocol SearchAPIActionType { var response: Observable<[Repository]> { get } var error: Observable<Error> { get } func execute(_ text: String) }
  87. Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class SearchAPIStream: UnioStream<SearchAPIStream.Logic>, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay<String>() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable<Error> } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency<Input, NoState, Extra>) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable<Event<[Repository]>> in let request = URLRequest(url: URL(string: "https://◦◦?q=\(query)")!) return extra.session.rx.data(request: request) .take(1) .map { try extra.decoder.decode([Repository].self, from:$0) } .materialize() } .share() return Output(response: result.flatMap { $0.element.map(Observable.just) ?? .empty() }, error: result.flatMap { $0.error.map(Observable.just) ?? .empty() }) } } } protocol SearchAPIActionType { var response: Observable<[Repository]> { get } var error: Observable<Error> { get } func execute(_ text: String) }
  88. Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class SearchAPIStream: UnioStream<SearchAPIStream.Logic>, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay<String>() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable<Error> } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency<Input, NoState, Extra>) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable<Event<[Repository]>> in let request = URLRequest(url: URL(string: "https://◦◦?q=\(query)")!) return extra.session.rx.data(request: request) .take(1) .map { try extra.decoder.decode([Repository].self, from:$0) } .materialize() } .share() return Output(response: result.flatMap { $0.element.map(Observable.just) ?? .empty() }, error: result.flatMap { $0.error.map(Observable.just) ?? .empty() }) } } } protocol SearchAPIActionType { var response: Observable<[Repository]> { get } var error: Observable<Error> { get } func execute(_ text: String) } *OQVUͱ0VUQVUͷΈΛఆٛ͠ .PDLԽͰ͖ΔΑ͏ʹ͢Δ
  89. Model΋UnioStreamԽ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction: SearchAPIStreamType } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let apiAction = dependency.extra.apiAction apiAction.output.observable(for: \.response) .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .bind(to: apiAction.input.accept(for: \.execute)) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } }
  90. Model΋UnioStreamԽ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction: SearchAPIStreamType } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let apiAction = dependency.extra.apiAction apiAction.output.observable(for: \.response) .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .bind(to: apiAction.input.accept(for: \.execute)) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } ςετ࣌ʹΛ.PDLΛ౉ͤΔΑ͏ʹ͢Δ
  91. Model΋UnioStreamԽ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction: SearchAPIStreamType } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let apiAction = dependency.extra.apiAction apiAction.output.observable(for: \.response) .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .bind(to: apiAction.input.accept(for: \.execute)) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } PVUQVU͔ΒϨεϙϯεΛड͚औΔ
  92. Model΋UnioStreamԽ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction: SearchAPIStreamType } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let apiAction = dependency.extra.apiAction apiAction.output.observable(for: \.response) .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .bind(to: apiAction.input.accept(for: \.execute)) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } JOQVU͔ΒϦΫΤετΛ࣮ߦ͢Δ
  93. UnioStreamͷςετ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction: SearchAPIStreamType } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let apiAction = dependency.extra.apiAction apiAction.output.observable(for: \.response) .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .bind(to: apiAction.input.accept(for: \.execute)) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } }
  94. UnioStreamͷςετ class SearchViewStream: UnioStream<SearchViewStream.Logic> { struct Input: InputType { let

    search = PublishRelay<Void>() let text = PublishRelay<String?>() } struct Output: OutputType { let repositories: Observable<[Repository]> } struct State: StateType { let repositories = BehaviorRelay<[Repository]>(value: []) } struct Extra: ExtraType { let apiAction: SearchAPIStreamType } struct Logic: LogicType { let disposeBag = DisposeBag() func bind(from dependency: Dependency<Input, State, Extra>) -> Output { let state = dependency.state let apiAction = dependency.extra.apiAction apiAction.output.observable(for: \.response) .bind(to: state.repositories) .disposed(by: disposeBag) dependency.inputObservable(for: \.search) .withLatestFrom(dependency.inputObservable(for: \.text)) { $1 } .flatMap { $0.map(Observable.just) ?? .empty() } .bind(to: apiAction.input.accept(for: \.execute)) .disposed(by: disposeBag) return Output(repositories: state.repositories.asObservable()) } } } ݕࡧจࣈྻ͕౉͍ͬͯΔঢ়ଶͰݕࡧ͕࣮ߦ͞ΕΔͱ "1*ͷFYFDVUF͕ݺ͹ΕΔςετ
  95. UnioStreamͷςετ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class MockSearchAPIStream: SearchAPIStreamType { let input: InputWrapper<SearchAPIStream.Input> let output: OutputWrapper<SearchAPIStream.Output> let _input = SearchAPIStream.Input() let _response = PublishRelay<[Repository]>() let _error = PublishRelay<Error>() init() { self.input = InputWrapper(_input) let output = SearchAPIStream.Output( response: _response.asObservable(), error: _error.asObservable() ) self.output = OutputWrapper(output) } }
  96. UnioStreamͷςετ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class MockSearchAPIStream: SearchAPIStreamType { let input: InputWrapper<SearchAPIStream.Input> let output: OutputWrapper<SearchAPIStream.Output> let _input = SearchAPIStream.Input() let _response = PublishRelay<[Repository]>() let _error = PublishRelay<Error>() init() { self.input = InputWrapper(_input) let output = SearchAPIStream.Output( response: _response.asObservable(), error: _error.asObservable() ) self.output = OutputWrapper(output) } } QSPUPDPMʹ߹ΘͤͯɺJOQVUͱPVUQVUΛఆٛ͠·͢
  97. UnioStreamͷςετ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class MockSearchAPIStream: SearchAPIStreamType { let input: InputWrapper<SearchAPIStream.Input> let output: OutputWrapper<SearchAPIStream.Output> let _input = SearchAPIStream.Input() let _response = PublishRelay<[Repository]>() let _error = PublishRelay<Error>() init() { self.input = InputWrapper(_input) let output = SearchAPIStream.Output( response: _response.asObservable(), error: _error.asObservable() ) self.output = OutputWrapper(output) } } *OQVU͕࣮ߦ͞Εͨ͜ͱΛड͚औΔͨΊʹ *OQVU8SBQQFS͚ͩͰͳ͘*OQVU΋ఆٛ
  98. UnioStreamͷςετ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper<SearchAPIStream.Input> { get

    } var output: OutputWrapper<SearchAPIStream.Output> { get } } class MockSearchAPIStream: SearchAPIStreamType { let input: InputWrapper<SearchAPIStream.Input> let output: OutputWrapper<SearchAPIStream.Output> let _input = SearchAPIStream.Input() let _response = PublishRelay<[Repository]>() let _error = PublishRelay<Error>() init() { self.input = InputWrapper(_input) let output = SearchAPIStream.Output( response: _response.asObservable(), error: _error.asObservable() ) self.output = OutputWrapper(output) } } QSPQFSUZͷ*OQVUΛ΋ͱʹ*OQVU8SBQQFSΛੜ੒
  99. UnioStreamͷςετ class SearchViewStreamTests: XCTestCase { var testTarget: SearchViewStream! var mockSearchAPIStream:

    MockSearchAPIStream! override func setUp() { self.mockSearchAPIStream = MockSearchAPIStream() self.testTarget = SearchViewStream( input: .init(), state: .init(), extra: .init(apiAction: mockSearchAPIStream), logic: .init() ) } func test_input_textʹจࣈྻΛ౉͠_searchΛ࣮ߦ͢Δͱ_execute͕ݺ͹ΕΔ() { let result = BehaviorRelay<String?>(value: nil) let disposable = mockSearchAPIStream._input.execute.bind(to: result) let expected = "test-query" testTarget.input.accept(expected, for: \.text) testTarget.input.accept((), for: \.search) XCTAssertEqual(result.value, expected) disposable.dispose() } }
  100. UnioStreamͷςετ class SearchViewStreamTests: XCTestCase { var testTarget: SearchViewStream! var mockSearchAPIStream:

    MockSearchAPIStream! override func setUp() { self.mockSearchAPIStream = MockSearchAPIStream() self.testTarget = SearchViewStream( input: .init(), state: .init(), extra: .init(apiAction: mockSearchAPIStream), logic: .init() ) } func test_input_textʹจࣈྻΛ౉͠_searchΛ࣮ߦ͢Δͱ_execute͕ݺ͹ΕΔ() { let result = BehaviorRelay<String?>(value: nil) let disposable = mockSearchAPIStream._input.execute.bind(to: result) let expected = "test-query" testTarget.input.accept(expected, for: \.text) testTarget.input.accept((), for: \.search) XCTAssertEqual(result.value, expected) disposable.dispose() } } ςετର৅ͷ6OJP4USFBNͱ.PDLԽͨ͠6OJP4USFBNΛ อ࣋͠ɺॳظԽ࣌ʹ&YUSBܦ༝Ͱ.PDLΛ஫ೖ͢Δ
  101. UnioStreamͷςετ class SearchViewStreamTests: XCTestCase { var testTarget: SearchViewStream! var mockSearchAPIStream:

    MockSearchAPIStream! override func setUp() { self.mockSearchAPIStream = MockSearchAPIStream() self.testTarget = SearchViewStream( input: .init(), state: .init(), extra: .init(apiAction: mockSearchAPIStream), logic: .init() ) } func test_input_textʹจࣈྻΛ౉͠_searchΛ࣮ߦ͢Δͱ_execute͕ݺ͹ΕΔ() { let result = BehaviorRelay<String?>(value: nil) let disposable = mockSearchAPIStream._input.execute.bind(to: result) let expected = "test-query" testTarget.input.accept(expected, for: \.text) testTarget.input.accept((), for: \.search) XCTAssertEqual(result.value, expected) disposable.dispose() } } *OQVUͷFYFDVUF͕࣮ߦ͞Εͨ৔߹ʹ஋Λड͚औΔ
  102. UnioStreamͷςετ class SearchViewStreamTests: XCTestCase { var testTarget: SearchViewStream! var mockSearchAPIStream:

    MockSearchAPIStream! override func setUp() { self.mockSearchAPIStream = MockSearchAPIStream() self.testTarget = SearchViewStream( input: .init(), state: .init(), extra: .init(apiAction: mockSearchAPIStream), logic: .init() ) } func test_input_textʹจࣈྻΛ౉͠_searchΛ࣮ߦ͢Δͱ_execute͕ݺ͹ΕΔ() { let result = BehaviorRelay<String?>(value: nil) let disposable = mockSearchAPIStream._input.execute.bind(to: result) let expected = "test-query" testTarget.input.accept(expected, for: \.text) testTarget.input.accept((), for: \.search) XCTAssertEqual(result.value, expected) disposable.dispose() } } จࣈྻΛ౉ͯ͠ɺݕࡧΛ࣮ߦ͢Δ
  103. UnioStreamͷςετ class SearchViewStreamTests: XCTestCase { var testTarget: SearchViewStream! var mockSearchAPIStream:

    MockSearchAPIStream! override func setUp() { self.mockSearchAPIStream = MockSearchAPIStream() self.testTarget = SearchViewStream( input: .init(), state: .init(), extra: .init(apiAction: mockSearchAPIStream), logic: .init() ) } func test_input_textʹจࣈྻΛ౉͠_searchΛ࣮ߦ͢Δͱ_execute͕ݺ͹ΕΔ() { let result = BehaviorRelay<String?>(value: nil) let disposable = mockSearchAPIStream._input.execute.bind(to: result) let expected = "test-query" testTarget.input.accept(expected, for: \.text) testTarget.input.accept((), for: \.search) XCTAssertEqual(result.value, expected) disposable.dispose() } } FYFDVUF͕ݺͼग़͞Εɺର৅΋จࣈྻ͕౉͞Ε͍ͯΔ
  104. Input - KeyPath Dynamic Member Lookup struct Input: InputType {

    let search = PublishRelay<String>() } let input = InputWrapper(Input()) input.search("GitHub") input.search.onNext("Unio") let c: (String) -> Void = input.search let o: AnyObserver<String> = input.search
  105. Input - KeyPath Dynamic Member Lookup struct Input: InputType {

    let search = PublishRelay<String>() } let input = InputWrapper(Input()) input.search("GitHub") input.search.onNext("Unio") let c: (String) -> Void = input.search let o: AnyObserver<String> = input.search QSPQFSUZʹ௚઀ΞΫηε͍ͯ͠Δ͔ͷΑ͏ͳݺͼग़͕͠Ͱ͖Δ
  106. @dynamicMemberLookup class InputWrapper<Input: InputType> { private let input: T subscript<T>(dynamicMember

    keyPath: KeyPath<Input, PublishRelay<T>) -> (T) -> Void { return input[keyPath: keyPath].accept } subscript<T>(dynamicMember keyPath: KeyPath<Input, PublishRelay<T>>) -> AnyObserver<T> { let relay = input[keyPath: keyPath] return AnyObserver { $0.element.map(relay.accept) } } } Input - KeyPath Dynamic Member Lookup
  107. @dynamicMemberLookup class InputWrapper<Input: InputType> { private let input: T subscript<T>(dynamicMember

    keyPath: KeyPath<Input, PublishRelay<T>) -> (T) -> Void { return input[keyPath: keyPath].accept } subscript<T>(dynamicMember keyPath: KeyPath<Input, PublishRelay<T>>) -> AnyObserver<T> { let relay = input[keyPath: keyPath] return AnyObserver { $0.element.map(relay.accept) } } } Input - KeyPath Dynamic Member Lookup ,FZ1BUIEZOBNJD.FNCFS-PPLVQʹରԠ
  108. struct Output: OutputType { let title = BehaviorRelay(value: "title") }

    let output = OutputWrapper(Output()) let o: Observable<String> = output.title let v: String = output.title.value let p: Property<String> = output.title Output - KeyPath Dynamic Member Lookup
  109. struct Output: OutputType { let title = BehaviorRelay(value: "title") }

    let output = OutputWrapper(Output()) let o: Observable<String> = output.title let v: String = output.title.value let p: Property<String> = output.title Output - KeyPath Dynamic Member Lookup QSPQFSUZʹ௚઀ΞΫηε͍ͯ͠Δ͔ͷΑ͏ͳݺͼग़͕͠Ͱ͖Δ
  110. let stream: CounterViewStreamType let button: UIButton let label: UILabel button.rx.tap

    .bind(to: stream.input.accept(for: \.increment)) .disposed(by: disposeBag) stream.output.observable(for: \.text) .bind(to: label.rx.text) .disposed(by: disposeBag) Unio - KeyPath Dynamic Member Lookup
  111. let stream: CounterViewStreamType let button: UIButton let label: UILabel button.rx.tap

    .bind(to: stream.input.accept(for: \.increment)) .disposed(by: disposeBag) stream.output.observable(for: \.text) .bind(to: label.rx.text) .disposed(by: disposeBag) Unio - KeyPath Dynamic Member Lookup button.rx.tap .bind(to: stream.input.increment) .disposed(by: disposeBag) stream.output.text .bind(to: label.rx.text) .disposed(by: disposeBag)