Slide 1

Slide 1 text

MVVM + Flux 2017/04/26 marty-suzuki

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

ୈ5ষ ݱ৔Ͱ໾ཱͭ ϞόΠϧΞϓϦઃܭɾ։ൃ 5-2 λΠϓηʔϑͰϞμϯͳiOSΞϓϦͷઃܭ

Slide 4

Slide 4 text

ͦΕͰ͸ૣ଎...

Slide 5

Slide 5 text

MVVM

Slide 6

Slide 6 text

MVVM • Model ϏδωεϩδοΫΛ࣋ͭ • View Ϣʔβʔ͔ΒͷೖྗΛड͚औΔ • ViewModel ϓϨθϯςʔγϣϯʹؔ͢ΔϩδοΫɾঢ়ଶΛ࣋ͭ

Slide 7

Slide 7 text

MVVM Figure 1

Slide 8

Slide 8 text

Flux

Slide 9

Slide 9 text

Flux ΞϓϦ಺ͷσʔλϑϩʔͷ୯ํ޲ԽΛ͢Δ • Action ViewͳͲ͔Βͷૢ࡞ʹΑΓΠϕϯτΛൃੜͤ͞Δ • Dispatcher Action͔ΒྲྀΕ͖ͯͨΠϕϯτΛStoreʹྲྀ͢ • Store Dispatcher͔ΒྲྀΕ͖ͯͨΠϕϯτ಺ͷ஋Λอ࣋͢Δ

Slide 10

Slide 10 text

Flux Figure 2

Slide 11

Slide 11 text

͜͜·Ͱ͸͓͞Β͍

Slide 12

Slide 12 text

ຊ୊͸...

Slide 13

Slide 13 text

MVVM + Flux

Slide 14

Slide 14 text

MVVM + Flux ϓꙎ ϹωЀϓЄτϴЀϺυϐμΨView͔Β෼཭Ͱ͖Δ • Action • Dispatcher • Store • ViewModel ΠϕϯτͷൃՐΛ͠ɺStoreͰͷมߋΛड͚औΔ

Slide 15

Slide 15 text

MVVM + Flux Figure 3

Slide 16

Slide 16 text

ඦฉ͸Ұݟʹ೗͔ͣ

Slide 17

Slide 17 text

͜ΕΒΛ༻͍ͯ αϯϓϧΞϓϦΛ࡞੒͍͖ͯ͠·͢

Slide 18

Slide 18 text

࣮ݱ͍ͨ͜͠ͱ • QiitaʹϩάΠϯ • ΞΫηετʔΫϯ͕੾ΕͨΒɺϩάΠϯը໘ʹ໭͢ • ౤ߘΛݕࡧ • 0.3ඵͷؒʹจࣈʹೖྗ͕ͳ͔ͬͨΒݕࡧ • ԼʹεΫϩʔϧ͍ͯ͘͠ͱࣗಈಡࠐ • ౤ߘΛӾཡ

Slide 19

Slide 19 text

QiitaWithFluxSample https://github.com/marty-suzuki/ QiitaWithFluxSample AbemaTVͰ࠾༻͍ͯ͠Δઃܭʹ͍ۙ͠ ΋ͷʹ͍ͯ͠·͢ɻ

Slide 20

Slide 20 text

ը໘ߏ੒ • RootViewController • LoginNavigationController • LoginTopViewController • LoginViewController • SearchNavigationController • SearchTopViewController

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

Flux ActionͱStoreͷ1ηοτ͝ͱʹɺDispatcher΋1ͭ • Application • ApplicationAction • ApplicationDispatcher • ApplicationStore • Route • RouteAction • RouteDispatcher • RouteStore

Slide 23

Slide 23 text

Application • ApplicationAction func requestAccessToken(withCode code: String) func removeAccessToken() • ApplicationStore let accessToken: Property let accessTokenError: Observable

Slide 24

Slide 24 text

Propertyͱ͸ https://github.com/inamiy/RxProperty set΍bind͕Ͱ͖ͳ͍Variable final class ViewModel { let intValue: Property private let _intValue = Variable(0) public init() { self.intValue = Property(_intValue) } } viewModel.intValue.value = 1 // error // Observable observable.bind(to: viewModel.intValue) // error

Slide 25

Slide 25 text

RouteAction • RouteAction func show(loginDisplayType: LoginDisplayType) func show(searchDisplayType: SearchDisplayType) • RouteStore let login: Observable let search: Observable

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

RootViewModel final class RootViewModel { private let disposeBag = DisposeBag() let login: Observable let search: Observable init(applicationStore: ApplicationStore = .shared, routeAction: RouteAction = .shared, routeStore: RouteStore = .shared) { let accessTokenObservable = applicationStore.accessToken.asObservable() // AccessToken͕nilͰͳ͍ͱ͖ɺSearchͷTopΛදࣔ͢ΔΞΫγϣϯΛ౤͛Δ accessTokenObservable.filter { $0 != nil }.map { _ in SearchDisplayType.root } .bind(onNext: routeAction.show).addDisposableTo(disposeBag) // AccessToken͕nilͷͱ͖ʹɺLoginը໘Λදࣔ͢ΔΞΫγϣϯΛ౤͛Δ accessTokenObservable.filter { $0 == nil }.map { _ in LoginDisplayType.root } .bind(onNext: routeAction.show).addDisposableTo(disposeBag) // ViewControllerͰsubscribe͢Δ༻ͷObservable self.login = routeStore.login self.search = routeStore.search } }

Slide 28

Slide 28 text

RootViewController // ViewController಺ͰViewModelͷobservableΛsubscribe͠ɺը໘ભҠΛߦ͏ // ॳճىಈ࣌͸AccessToken͕ͳ͍ͷͰLoginը໘Λදࣔ͢Δ viewModel.login .observeOn(ConcurrentMainScheduler.instance) .filterNil() .subscribe(onNext: { [weak self] displayType in guard let me = self else { return } let loginNC: LoginNavigationController if let nc = me.currentViewController as? LoginNavigationController { loginNC = nc } else { loginNC = LoginNavigationController() me.currentViewController = loginNC } switch displayType { case .root: if loginNC.topViewController is LoginTopViewController { return } loginNC.popToRootViewController(animated: true) case .webView: if loginNC.topViewController is LoginViewController { return } loginNC.pushViewController(LoginViewController.instantiate(), animated: true) } }) .addDisposableTo(disposeBag)

Slide 29

Slide 29 text

LoginViewModel final class LoginViewModel { let isLoading: Property private let _isLoading = Variable(false) let authorizeRequest: OauthAuthorizeRequest let authorizeUrl: URL private let applicationAction: ApplicationAction private let disposeBag = DisposeBag() init(applicationStore: ApplicationStore = .shared, applicationAction: ApplicationAction = .shared, config: Config = .shared) { self.applicationAction = applicationAction // and so on... } func requestAccessToken(withCode code: String) { // and so on... // AccessTokenͷϦΫΤετΛ͠ɺऔಘ͕׬ྃ͢ΔͱDispatcherΛհ͠Store΁ applicationAction.requestAccessToken(withCode: code) } }

Slide 30

Slide 30 text

LoginViewController class LoginViewController: UIViewController, Storyboardable { let loadingView = LoadingView(indicatorStyle: .whiteLarge) let webView: WKWebView = WKWebView(frame: .zero) private let viewModel = LoginViewModel() private(set) lazy var dataSource: LoginViewDataSource = .init(viewModel: self.viewModel) private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() configureWebView() // and so on... } private func configureWebView() { // navigationDelegateΛLoginViewDataSourceͱ͢Δ webView.navigationDelegate = dataSource // and so on... } }

Slide 31

Slide 31 text

LoginViewDataSource final class LoginViewDataSource: WKNavigationDelegate { let viewModel: LoginViewModel init(viewModel: LoginViewModel) { self.viewModel = viewModel } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) { guard let url = navigationAction.request.url else { decisionHandler(.cancel) return } if url.absoluteString.hasPrefix(Config.shared.redirectUrl) { guard let URLComponents = URLComponents(string: url.absoluteString), // and so on... let code = codeItem.value else { fatalError("can not find \"code\" from URL query") } // viewModel͔ΒAccessTokenΛϦΫΤετ viewModel.requestAccessToken(withCode: code) decisionHandler(.cancel) return } decisionHandler(.allow) } }

Slide 32

Slide 32 text

RootViewController // AccessTokenΛऔಘͨ͠ޙʹɺݕࡧը໘Λදࣔ͢Δ viewModel.search .observeOn(ConcurrentMainScheduler.instance) .filterNil() .subscribe(onNext: { [weak self] displayType in guard let me = self else { return } let searchNC: SearchNavigationController if let nc = me.currentViewController as? SearchNavigationController { searchNC = nc } else { searchNC = SearchNavigationController() me.currentViewController = searchNC } switch displayType { case .root: if searchNC.topViewController is SearchTopViewController { return } searchNC.popToRootViewController(animated: true) case .webView(let url): if searchNC.topViewController is SFSafariViewController { return } searchNC.pushViewController(SFSafariViewController(url: url), animated: true) } }) .addDisposableTo(disposeBag)

Slide 33

Slide 33 text

SearchTopViewModel // ݕࡧը໘Ͱར༻͢Δσʔλ͸ɺ͜ͷը໘಺Ͱऩ·ΔͷͰSearchͷFluxΦϒδΣΫτ͸࡞Βͳ͍ // ϓϨθϯςʔγϣϯʹؔ͢Δঢ়ଶΛ࣋ͪɺActionΛར༻ͯ͠Apiͱͷ௨৴Λߦ͏ final class SearchTopViewModel { let lastItemsRequest: Property private let _lastItemsRequest = Variable(nil) let items: Property<[Item]> private let _items = Variable<[Item]>([]) let totalCount: Property private let _totalCount = Variable(0) let error: Property private let _error = Variable(nil) let hasNext: Property private let _hasNext = Variable(true) let searchAction: Action> private let perPage: Int = 20 let noResult: Observable let reloadData: Observable let isFirstLoading: Observable let keyboardWillShow: Observable let keyboardWillHide: Observable }

Slide 34

Slide 34 text

Actionͱ͸ https://github.com/RxSwiftCommunity/Action enable͕trueͷͱ͖ͷΈʹ࣮ߦͰ͖Δ let searchAction = Action> { request in return QiitaSession.shared.send(request) } let request = ItemsRequest(page: nextPage, perPage: perPage, query: nextQuery) searchAction.execute(request) searchAction.elements .subscribe(onNext: { items: ElementsResponse in // and so on }) .addDisposableTo(disposeBag) searchAction.errors searchAction.executing searchAction.enabled

Slide 35

Slide 35 text

SearchTopViewModel // ViewController͔ΒͷΠϕϯτΛड͚औΓɺViewModel಺ͷΞΫγϣϯΛݺͿ func observe(textControlProperty: ControlProperty, deleteButtonTap: ControlEvent, reachedBottom: Observable) { externalDisposeBag = DisposeBag() textControlProperty.orEmpty .distinctUntilChanged() .debounce(0.3, scheduler: ConcurrentMainScheduler.instance) .subscribe(onNext: { [weak self] text in self?.search(query: text) }) .addDisposableTo(externalDisposeBag) deleteButtonTap .subscribe(onNext: { [weak self] in self?.removeAccessToken() }) .addDisposableTo(externalDisposeBag) reachedBottom .subscribe(onNext: { [weak self] in self?.search() }) .addDisposableTo(externalDisposeBag) }

Slide 36

Slide 36 text

·ͱΊ • ΞϓϦશମͰ࢖͏Α͏ͳσʔλΛѻ͏ MVVM + Flux • 1ը໘Ͱऩ·ΔσʔλΛѻ͏ MVVM

Slide 37

Slide 37 text

͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ marty-suzuki