Slide 1

Slide 1 text

MVVMͷ࣮૷ΛറΔFrameworkΛ։ൃɾಋೖ͠ νʔϜͰ͹Β͖͕࣮ͭ͋ͬͨ૷Λ౷Ұ͢Δ iOSDC Japan 2019 Reject Conference days1 @marty_suzuki

Slide 2

Slide 2 text

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.

Slide 3

Slide 3 text

͸͡Ίʹ MVVMͰ։ൃͨ͜͠ͱ͕͋Δ

Slide 4

Slide 4 text

͸͡Ίʹ ։ൃऀʹΑͬͯ ViewModelͷ࣮૷ํ๏ʹҧ͍͕͋Δ ͱײ͡·ͤΜ͔ʁ

Slide 5

Slide 5 text

ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } }

Slide 6

Slide 6 text

ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } } ϩδοΫͷ࣮૷

Slide 7

Slide 7 text

ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } } ֎෦͔Βͷೖྗ

Slide 8

Slide 8 text

ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } } ೖྗͷϦϨʔ

Slide 9

Slide 9 text

ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } } ಺෦ঢ়ଶ

Slide 10

Slide 10 text

ྫ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } } ֎෦΁ͷग़ྗ

Slide 11

Slide 11 text

ྫ - 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)

Slide 12

Slide 12 text

ྫ - 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) ग़ྗͷ൓ө

Slide 13

Slide 13 text

ྫ - 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) ϢʔβʔΞΫγϣϯͷ఻ୡ

Slide 14

Slide 14 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } }

Slide 15

Slide 15 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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ͷϩʔΧϧม਺ͱͯ͠ ఆٛ͢Δ͜ͱ΋Ͱ͖Δ

Slide 16

Slide 16 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private let _search = PublishRelay() private let _text = PublishRelay() 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) } }

Slide 17

Slide 17 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶃ class SearchViewModel { let repositories: Observable<[Repository]> private let _search = PublishRelay() private let _text = PublishRelay() 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ͷϩʔΧϧม਺ͱͯ͠ ఆٛ͢Δ͜ͱ΋Ͱ͖Δ

Slide 18

Slide 18 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } }

Slide 19

Slide 19 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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Ͱఆٛ͢Δ͜ͱ΋Ͱ͖Δ

Slide 20

Slide 20 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let search: AnyObserver let setText: AnyObserver let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishSubject() private let _setText = PublishSubject() 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) } }

Slide 21

Slide 21 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ class SearchViewModel { let search: AnyObserver let setText: AnyObserver let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishSubject() private let _setText = PublishSubject() 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Ͱఆٛ͢Δ͜ͱ΋Ͱ͖Δ

Slide 22

Slide 22 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ 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)

Slide 23

Slide 23 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶄ 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 ʹͳΔ

Slide 24

Slide 24 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ class SearchViewModel { let repositories: Observable<[Repository]> private let disposeBag = DisposeBag() init(search: Observable, setText: Observable) { 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) } }

Slide 25

Slide 25 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ class SearchViewModel { let repositories: Observable<[Repository]> private let disposeBag = DisposeBag() init(search: Observable, setText: Observable) { 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ͷҾ਺ͰೖྗΛड͚औΔ

Slide 26

Slide 26 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ 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)

Slide 27

Slide 27 text

ྫ - ࣮૷ʹ͕ࠩग़΍͍͢Օॴᶅ 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ଆʹ͸ͳ͍

Slide 28

Slide 28 text

ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } }

Slide 29

Slide 29 text

ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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 @ ͕ݺ΂ͯ͠·͏

Slide 30

Slide 30 text

ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } } ೖྗͷϦϨʔ༻ʹಉ͡Α͏ͳఆٛΛ ࠶౓͠ͳ͚Ε͹ͳΒͳ͍

Slide 31

Slide 31 text

ྫ - ࣮૷Ͱɺͦͷଞͷ΋Ͳ͔͍͠Օॴ class SearchViewModel { let repositories: Observable<[Repository]> private let _repositories = BehaviorRelay<[Repository]>(value: []) private let _search = PublishRelay() private let _text = PublishRelay() 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) } } ಛͷఆٛ৔ॴ͕ܾ·͍ͬͯͳ͍ ͷͰΫϥε಺ͷͲ͜ʹͰ΋ఆٛ Ͱ͖ͯ͠·͏

Slide 32

Slide 32 text

ViewModel͕େ͖͘ͳ͖ͬͯͨͱ͖ʹ 1. ࣮૷ํ๏ʹҧ͍͕͋Δ 2. ೖग़ྗ͕୯ํ޲ʹͳͬͯͳ͍ ݁ߏਏ͘ͳ͍Ͱ͔͢

Slide 33

Slide 33 text

IUUQTHJUIVCDPNDBUTPTT6OJP

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Input - Unioͷߏ੒ཁૉ struct Input: InputType { let search = PublishRelay() let selectIndex = PublishSubject() }

Slide 37

Slide 37 text

Input - Unioͷߏ੒ཁૉ struct Input: InputType { let search = PublishRelay() let selectIndex = PublishSubject() } 7JFX.PEFM΁ͷೖྗΛఆٛ͢Δ৔ॴ

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

let input: InputWrapper input.accept("GitHub", for: \.search) Input - Unioͷߏ੒ཁૉ struct Input: InputType { let search = PublishRelay() let selectIndex = PublishSubject() } 1VCMJTI3FMBZͰఆٛ͞Ε͍ͯΔ͕ GVODBDDFQU @ ͔࣮͠ߦͰ͖ͳ͍ʂ

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

InputWrapperͷ࢓૊Έ class InputWrapper { private let input: Input func accept(_ value: T, for keyPath: KeyPath>) { input[keyPath: keyPath].accept(value) } } *OQVUͰ1VCMJTI3FMBZ5Ͱఆٛ͞Ε͍ͯΔ QSPQFSUZͷ,FZ1BUIΛड͚औΔ struct Input: InputType { let search = PublishRelay() let selectIndex = PublishSubject() } let input: InputWrapper input.accept("GitHub", for: \.search)

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Output - Unioͷߏ੒ཁૉ struct Output: OutputType { let isHidden = BehaviorRelay(value: false) let title = BehaviorSubject(value: "title") } 7JFX.PEFM΁ͷग़ྗΛఆٛ͢Δ৔ॴ

Slide 49

Slide 49 text

Output - Unioͷߏ੒ཁૉ let output: OutputWrapper 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") }

Slide 50

Slide 50 text

Output - Unioͷߏ੒ཁૉ let output: OutputWrapper 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ʹ ϥοϓ͞Εͯެ։͞ΕΔ

Slide 51

Slide 51 text

Output - Unioͷߏ੒ཁૉ let output: OutputWrapper 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 ͔࣮͠ߦͰ͖ͳ͍ʂ

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

class OutputWrapper { private let output: Output func value(for keyPath: KeyPath>) -> T { return output[keyPath: keyPath].value } func observable(for keyPath: KeyPath>) -> Observable { 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 let label: UILabel label.isHidden = output.value(for: \.isHideen) output.observable(for: \.isHideen).bind(to: label.rx.isHidden)

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Output - Unioͷߏ੒ཁૉ let output: OutputWrapper 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") }

Slide 59

Slide 59 text

Output - Unioͷߏ੒ཁૉ let output: OutputWrapper 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͢Δ͜ͱͰΞΫηεͰ͖Δ

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

State - Unioͷߏ੒ཁૉ struct State: StateType { let count = BehaviorRelay(value: 0) }

Slide 62

Slide 62 text

State - Unioͷߏ੒ཁૉ struct State: StateType { let count = BehaviorRelay(value: 0) } 7JFX.PEFMͷ಺෦ঢ়ଶΛఆٛ͢Δ৔ॴ

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Extra - Unioͷߏ੒ཁૉ struct Extra: ExtraType { let userDefaults: UserDefaults let notificationCenter: NotificationCenter }

Slide 65

Slide 65 text

Extra - Unioͷߏ੒ཁૉ struct Extra: ExtraType { let userDefaults: UserDefaults let notificationCenter: NotificationCenter } 7JFX.PEFMͷ֎෦ґଘΛఆٛ͢Δ৔ॴ

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency) -> 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() } struct Output: OutputType { let text: Observable } struct State: StateType { let count = BehaviorRelay(value: 0) }

Slide 68

Slide 68 text

Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency) -> 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() } struct Output: OutputType { let text: Observable } struct State: StateType { let count = BehaviorRelay(value: 0) } *OQVUɾ4UBUFɾ&YUSBΛར༻ͯ͠ 0VUQVUΛੜ੒Ͱ͖Δ།Ұͷ৔ॴ

Slide 69

Slide 69 text

Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency) -> 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() } struct Output: OutputType { let text: Observable } struct State: StateType { let count = BehaviorRelay(value: 0) } *OQVUɾ4UBUFɾ&YUSBʹΞΫηεͰ͖Δ

Slide 70

Slide 70 text

Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency) -> 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() } struct Output: OutputType { let text: Observable } struct State: StateType { let count = BehaviorRelay(value: 0) } %FQFOEFODZΛհͯ͠ɺ*OQVUͷ೚ҙͷ1VCMJTI3FMBZʹ ରͯ͠0CTFSWBCMFͱͯ͠ΞΫηε͢Δ͜ͱ͕Ͱ͖Δ ΋ͪΖΜɺGVODBDDFQU @ ͷݺͼग़͠͸Ͱ͖ͳ͍

Slide 71

Slide 71 text

Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency) -> 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() } struct Output: OutputType { let text: Observable } struct State: StateType { let count = BehaviorRelay(value: 0) } %FQFOEFODZΛհͯ͠ɺ4UBUFʹΞΫηεͰ͖Δ

Slide 72

Slide 72 text

Logic - Unioͷߏ੒ཁૉ struct Logic: LogicType { private let disposeBag = DisposeBag() func bind(from dependency: Dependency) -> 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() } struct Output: OutputType { let text: Observable } struct State: StateType { let count = BehaviorRelay(value: 0) } *OQVUɾ4UBUFɾ&YUSBΛར༻ͯ͠0VUQVUΛੜ੒

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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ͷߏ੒ཁૉ

Slide 77

Slide 77 text

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Λ հͯ͠ϘλϯͷλοϓΛೖྗͱͯ͠఻ୡ͢Δ

Slide 78

Slide 78 text

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Λ հͯ͠ग़ྗΛϥϕϧʹ൓ө͢Δ

Slide 79

Slide 79 text

UnioStreamͷ࢓૊Έ open class UnioStream { public let input: InputWrapper public let output: OutputWrapper 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 } }

Slide 80

Slide 80 text

UnioStreamͷ࢓૊Έ open class UnioStream { public let input: InputWrapper public let output: OutputWrapper 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ͱͯ͠อ͍࣋ͯ͠Δ

Slide 81

Slide 81 text

UnioStreamͷ࢓૊Έ open class UnioStream { public let input: InputWrapper public let output: OutputWrapper 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͕ੜ੒͞ΕΔ

Slide 82

Slide 82 text

UnioStreamͷ࢓૊Έ open class UnioStream { public let input: InputWrapper public let output: OutputWrapper 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ͷೖग़ྗΛ֬ఆ͢Δ

Slide 83

Slide 83 text

UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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() private let _text = PublishRelay() 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) } }

Slide 84

Slide 84 text

UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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() private let _text = PublishRelay() 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) } }

Slide 85

Slide 85 text

UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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() private let _text = PublishRelay() 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) } }

Slide 86

Slide 86 text

UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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() private let _text = PublishRelay() 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) } }

Slide 87

Slide 87 text

UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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() private let _text = PublishRelay() 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) } }

Slide 88

Slide 88 text

UnioͱViewModelͷൺֱ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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() private let _text = PublishRelay() 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) } }

Slide 89

Slide 89 text

UnioͰͰ͖ΔΑ͏ʹͳͬͨ͜ͱ • ೖग़ྗɾ಺෦ঢ়ଶͷఆٛ৔ॴ͕໌ ֬ʹͳͬͨ • ྨࣅͨ͠ೖྗΛෳ਺ఆٛ͢Δඞཁ ͕ͳ͘ͳͬͨ • WrapperͱDependencyʹΑͬ ͯɺ୯Ұํ޲ͷೖྗΛܕͰറΔ͜ ͱ͕Ͱ͖ΔΑ͏ʹͳͬͨ

Slide 90

Slide 90 text

Model΋UnioStreamԽ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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()) } } }

Slide 91

Slide 91 text

Model΋UnioStreamԽ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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()) } } }

Slide 92

Slide 92 text

Model΋UnioStreamԽ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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 { get } func execute(_ text: String) }

Slide 93

Slide 93 text

Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper { get } var output: OutputWrapper { get } } class SearchAPIStream: UnioStream, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable> 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 { get } func execute(_ text: String) }

Slide 94

Slide 94 text

Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper { get } var output: OutputWrapper { get } } class SearchAPIStream: UnioStream, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable> 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 { get } func execute(_ text: String) }

Slide 95

Slide 95 text

Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper { get } var output: OutputWrapper { get } } class SearchAPIStream: UnioStream, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable> 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 { get } func execute(_ text: String) }

Slide 96

Slide 96 text

Model΋UnioStreamԽ protocol SearchAPIStreamType: AnyObject { var input: InputWrapper { get } var output: OutputWrapper { get } } class SearchAPIStream: UnioStream, SearchAPIStreamType { struct Input: InputType { let execute = PublishRelay() } struct Output: OutputType { let response: Observable<[Repository]> let error: Observable } struct Extra: ExtraType { let session = URLSession.shared let decoder = JSONDecoder() } struct Logic: LogicType { func bind(from dependency: Dependency) -> Output { let extra = dependency.extra let result = dependency.inputObservable(for: \.execute) .flatMapLatest { query -> Observable> 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 { get } func execute(_ text: String) } *OQVUͱ0VUQVUͷΈΛఆٛ͠ .PDLԽͰ͖ΔΑ͏ʹ͢Δ

Slide 97

Slide 97 text

Model΋UnioStreamԽ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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()) } } }

Slide 98

Slide 98 text

Model΋UnioStreamԽ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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Λ౉ͤΔΑ͏ʹ͢Δ

Slide 99

Slide 99 text

Model΋UnioStreamԽ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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͔ΒϨεϙϯεΛड͚औΔ

Slide 100

Slide 100 text

Model΋UnioStreamԽ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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͔ΒϦΫΤετΛ࣮ߦ͢Δ

Slide 101

Slide 101 text

UnioStreamͷςετ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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()) } } }

Slide 102

Slide 102 text

UnioStreamͷςετ class SearchViewStream: UnioStream { struct Input: InputType { let search = PublishRelay() let text = PublishRelay() } 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) -> 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͕ݺ͹ΕΔςετ

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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(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Λ஫ೖ͢Δ

Slide 109

Slide 109 text

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(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͕࣮ߦ͞Εͨ৔߹ʹ஋Λड͚औΔ

Slide 110

Slide 110 text

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(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() } } จࣈྻΛ౉ͯ͠ɺݕࡧΛ࣮ߦ͢Δ

Slide 111

Slide 111 text

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(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͕ݺͼग़͞Εɺର৅΋จࣈྻ͕౉͞Ε͍ͯΔ

Slide 112

Slide 112 text

UnioͰͰ͖ΔΑ͏ʹͳͬͨ͜ͱ • Extraͱͯ͠UnioStreamΛ౉͢͜ ͱͰɺMockԽ͕༰қʹͳΔ

Slide 113

Slide 113 text

Unioͷෆศͳͱ͜Ζᶃ InputɾOutputɾStateɾExtra UnioStreamΛ ౎౓ఆٛ͠ͳ͍ͱ͍͚ͳ͍

Slide 114

Slide 114 text

Unioͷෆศͳͱ͜Ζᶃ - վળࡦ xctemplateͰੜ੒ͯ͠͠·͏

Slide 115

Slide 115 text

Unioͷෆศͳͱ͜Ζᶄ input.accept(for: \.hoge) output.observable(for: \.fuga) ͷΑ͏ʹKeyPathͰΞΫηε ͠ͳ͚Ε͹͍͚ͳ͍

Slide 116

Slide 116 text

IUUQTUXJUUFSDPNBLLZJFTUBUVT

Slide 117

Slide 117 text

IUUQTHJUIVCDPNBQQMFTXJGUFWPMVUJPO CMPCNBTUFSQSPQPTBMTLFZQBUI EZOBNJDNFNCFSMPPLVQNE

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

Input - KeyPath Dynamic Member Lookup struct Input: InputType { let search = PublishRelay() } let input = InputWrapper(Input()) input.search("GitHub") input.search.onNext("Unio") let c: (String) -> Void = input.search let o: AnyObserver = input.search QSPQFSUZʹ௚઀ΞΫηε͍ͯ͠Δ͔ͷΑ͏ͳݺͼग़͕͠Ͱ͖Δ

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

@dynamicMemberLookup class InputWrapper { private let input: T subscript(dynamicMember keyPath: KeyPath) -> (T) -> Void { return input[keyPath: keyPath].accept } subscript(dynamicMember keyPath: KeyPath>) -> AnyObserver { let relay = input[keyPath: keyPath] return AnyObserver { $0.element.map(relay.accept) } } } Input - KeyPath Dynamic Member Lookup ,FZ1BUIEZOBNJD.FNCFS-PPLVQʹରԠ

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

struct Output: OutputType { let title = BehaviorRelay(value: "title") } let output = OutputWrapper(Output()) let o: Observable = output.title let v: String = output.title.value let p: Property = output.title Output - KeyPath Dynamic Member Lookup QSPQFSUZʹ௚઀ΞΫηε͍ͯ͠Δ͔ͷΑ͏ͳݺͼग़͕͠Ͱ͖Δ

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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)

Slide 126

Slide 126 text

࠷ޙʹ UnioɹͰ࣮૷Λറͬͯ ୯Ұํ޲ͷೖग़ྗͳMVVMΛ ࢼͯ͠Έ·ͤΜ͔

Slide 127

Slide 127 text

͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠