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

What/Why/How MVVM on iOS

numa08
July 26, 2019

What/Why/How MVVM on iOS

numa08

July 26, 2019
Tweet

More Decks by numa08

Other Decks in Technology

Transcript

  1. 8IBU w 7JFX w 6*7JFX΍6*7JFX$POUSPMMFSͷαϒΫϥεͰ࣮૷͞ΕΔ w Ϣʔβʔ͕ൃੜͤ͞ΔΞΫγϣϯ΍6*7JFX$POUSPMMFS΍ΞχϝʔγϣϯͷίʔϧόοΫΛड͚औͬͯ 7JFX.PEFMʹ௨஌͢Δɻ w 7JFX.PEFM͕௨஌ͨ͠಺༰Λը໘ʹඳը͢Δ

    w 7JFX.PEFM w 6*Ͱൃੜͨ͠ΠϕϯτΛड͚औͬͯ.PEFMͰ࣮૷͞ΕͨϏδωεϩδοΫͳͲΛݺͼग़͢ w .PEFM͔ΒಘΒΕͨσʔλΛը໘ͰදࣔͰ͖Δܗࣜʹม׵ͯ͠7JFXʹ఻͑Δ w .PEFM w ϏδωεϩδοΫͷ࣮૷΍%# 8FC"1*ͳͲͷ֎෦γεςϜͷ࿈ܞΛ࣮૷͢Δ w 7JFX.PEFM͔Β఻͑ΒΕͨΠϕϯτ΍ଞͷ.PEFMͰൃੜͨ͠ΠϕϯτΛτϦΨʔʹͯ͠ॲཧΛ։࢝͢Δ w ॲཧͷ݁ՌΛ7JFX.PEFMʹ௨஌͢Δ 7
  2. 'SBNFXPSLΛ෼͚ͨ w "1*$MJFOU w 4XBHHFSͰੜ੒Λͨ͠"1*ͷΫϥΠΞϯτίʔυ w .PEFM w .PEFM૚΍&OUJUZ 7JFX.PEFMΛؚΉ'SBNFXPSL

    w 6*,JU΍3Y$PDPBʹ͸جຊతʹґଘ͠ͳ͍ w "QQ w 7JFXʹؔ͢ΔίʔυΛؚΉ'SBNFXPSL w ը૾ͳͲͷϦιʔεΉؚΉ w .PEFMʹґଘ͸͢Δ͕.PEFMͷ࣮ଶʹ͸৮ΒͣʹQSPUPDPMΛ࢖͏ w .BJO w "QQ%FMFHBUFΛؚΜͩ'SBNFXPSL 16
  3. 7JFX.PEFMͷ࣮૷ public class HogeViewModel { // ύϥϝʔλʹड͚औΔߏ଄ମ // View Ͱൃੜ͢ΔΠϕϯτΛ௨஌͢Δ

    public struct Inputs { public let tapButton: Single<()> public let inputText: Single<String> public let viewDidLoad: Signal<()> } // ฦΓ஋ͱͯ͠ฦ͢ߏ଄ମ // Model Ͱൃੜͨ͠ΠϕϯτͳͲΛ௨஌ͯ͠ // View Ͱඳը͢Δ public struct Outputs { public let title: Driver<String> public let contents: Driver<[Item]> } 17
  4. 7JFX.PEFMͷ࣮૷ private let commentModel: CommentModelProtocol private let disposeBag = DisposeBag()

    public func bind(_ inputs: Inputs) -> Outputs { // UI Ͱൃੜͨ͠ΠϕϯτΛτϦΨʔʹͯ͠ // Model ͷϝιουΛݺͼग़ͯ͠ॲཧ͢Δ inputs .tapButton .withLatestFrom(inputs.inputText) .emit(onNext: { [weak self] text in self?.commentModel.post(comment: text) }) .disposed(by: disposeBag) // ൃੜ͢ΔΠϕϯτ͸Ϣʔβʔͷಈ࡞ʹՃ͑ͯ // UIViewController ͷίʔϧόοΫͷ͜ͱ΋͋Δ inputs .viewDidLoad .emit(onNext: commentModel.fetchComment) .disposed(by: disposeBag) 18
  5. 7JFX.PEFMͷ࣮૷ public class HogeViewModel { // ೖྗ public struct Inputs

    { public let tapButton: Single<()> public let inputText: Single<String> public let viewDidLoad: Signal<()> } // ग़ྗ public struct Outputs { public let title: Driver<String> public let contents: Driver<[Item]> } public func bind(_ inputs: Inputs) -> Outputs { // ೖྗΠϕϯτΛग़ྗΠϕϯτʹม׵͢Δ } } 20
  6. 7JFX.PEFMͷ࣮૷ w *OQVUT w 6*ૢ࡞΍6*7JFX$POUSPMMFSͷίʔϧόοΫͳͲ6*Ͱൃੜ͢ΔΠϕ ϯτΛ0CTFSWBCMFͰ7JFX.PEFMʹ௨஌͢Δ w 0VUQVUT w *OQVUTͰ௨஌͞ΕͨΠϕϯτ΍.PEFMͰ௨஌͞ΕͨΠϕϯτΛ

    7JFXͰදࣔͰ͖Δܗࣜͱͯ͠7JFXʹ௨஌͢Δ w CJOE w *OQVUTΛύϥϝʔλʹऔͬͯ.PEFMͷૢ࡞ΛߦͬͨΓ.PEFM͔Β σʔλΛऔಘͯ͠0VUQVUTΛฦ͢ 21
  7. 7JFX$POUSPMMFSͷ࣮૷ final class HogeViewController: UIViewController { // ը໘͕ભҠ͢Δલʹલͷը໘͔Β ViewModel ͷΠϯελϯεΛηοτͯ͠΋Β͏

    var viewModel: HogeViewModel! override func viewDidLoad() { super.viewDidLoad() // UIKit Ͱൃੜͨ͠Πϕϯτ΍ɺίʔϧόοΫΛ ViewModel ʹ఻͑Δ let outputs = viewModel.bind(.init( tapButton: button.rx.tap.asSignal(), inputText: text.rx.value.orEmpty.asSignal(), viewDidLoad: .just() )) // ViewModel ͕௨஌ͨ͠σʔλΛը໘ʹදࣔ͢Δ outputs .title .drive(rx[\.title]).disposed(by: disposeBag) } } 22
  8. .PEFMͷఆٛ public protocol ContentsModelProtocol { // σʔλͷग़ྗ͸ϓϩύςΟͰఆٛΛ͓ͯ͘͠ public var contents:

    Observable<[Content]> { get } // id ͳͲͰϑΟϧλʔ͕ඞཁͳ৔߹͸ϝιουͱ͓ͯ͘͠ public func postResult(for contentId: ContentId) -> Observable<Result<(), Error>> // σʔλΛऔಘ͢Δϝιουɻ݁Ռ͸ `contents` ͷೖͬͯ͘Δ public func fetchContents() // σʔλͷॻ͖ࠐΈΛߦ͏ϝιουɻ੒ޭɾࣦഊ͸ `postresult(for:)Ͱऔಘ͢Δ` public func post(content: Content) } 24
  9. .PEFMͷ࣮૷ public class ContentsModel: ContentsModelProtocol { // Observable ͷ࣮ଶ͸ BehaviorRelay

    ͱ͓ͯ͘͠ͱɺͱΓ͋͑ͣ࠷ॳͷ஋Λ௨஌͢Δ͜ͱ͕Ͱ͖Δ // ࠷ޙͷ஋΋ relay ͯ͘͠ΕΔͷͰɺ UI ͰදࣔΛ͢Δͱ͖ʹ౎߹͕ྑ͍ private let contentsSubject = BehaviorRelay<[Content]>(value: []) // Model ͷ֎ଆ͔Βҙਤ͠ͳ͍มߋΛͤ͞ͳ͍ͨΊʹ΋ɺ֎෦ʹ͸ Observable ͱͯ͠ެ։͢Δ public var contents: Observable<[Content]> { return contentsSubject.asObservable() } public func fetchContents() { apiClient .fetchContents // Single Ͱ݁Ռ͕௨஌͞ΕΔ .subscribe(onSuccess: contentsSubject.accept) // ݁ՌΛ௨஌͢Δ .disposed(by: disposeBag) } 25
  10. .PEFMͷ࣮૷ // Result Λ௨஌͢Ε͹ ViewModel Ͱ੒ޭɾࣦഊͷϋϯυϦϯά͕Ͱ͖Δ private let postResultSubject =

    PublishSubject<Result<ContentId, Error>>() // ಛఆͷ݁Ռ͚ͩʹڵຯ͕͋ΔͷͰɺ filter Λ࢖͏ public func postResult(for contentId: ContentId) -> Observable<Result<ContentId, Error>> { return postResultSubject.filter { $0 == contentId }.asObservable() } public func post(content: Content) { apiClient .post(content: Content) // Single Ͱ݁Ռ͕ಘΒΕΔ .map { Result<ContentId, Error> in .success($0) } .catchError { e in .just(.failure(e)) } .subscribe(onSuccess: postResultSubject.accept) // ݁ՌΛ௨஌͢Δ .disposed(by: disposeBag) } 26
  11. .PEFMͷ࣮૷ w ϝιου w "1*΁ΞΫηεΛ͢ΔͳͲͷಈ࡞͸ϝιουͰදݱΛ͢Δ w "1*ݺͼग़͠ͷ݁Ռ͸4JOHMFͰಘΒΕΔ͜ͱ͕ଟ͍ͷͰɺແݶͷ σʔλͷ0CTFSCBMF΁ม׵Λ͢Δඞཁ͕͋Δ w ϓϩύςΟ

    w σʔλͷऔΓग़͠͸ϓϩύςΟͰߦ͏ w ແݶͷ0CTFSWBCMFͱͯ͠௨஌͞Εͯ͘Δ w pMUFS͕ඞཁͳ৔߹͸ϝιουͱ͢Δ͚ΕͲɺجຊతͳߟ͑ํ͸ಉ͡ 27
  12. .PEFMͷ࣮૷ // ViewModel func bind(_ inputs: Inputs) -> Outputs {

    return .init( // ϘλϯͷλοϓΛτϦΨʔͱͯ࣍͠ͷը໘ͷ ViewModel ͷΠϯελϯεΛੜ੒͢Δ // Model ͷΠϯελϯε΋ඞཁͩͬͨΓ͢ΔͷͰɺ init Ͱ౉͢ goToNextPage: inputs .tapNextButton .map { _ in NextPageViewModel(model: NextPageMode.instance) } ) } // ViewController func viewDidLoad() { outputs .goToNextPage() // ViewModel ͕௨஌͞ΕͨΒ࣍ͷը໘΁ભҠ͢Δ .drive(onNext: { [weak self] vm in let next = NextPageViewController() next.viewModel = vm self?.present(next, animated: true) }) .disposed(by: disposeBag) } 29
  13. 7JFX.PEFM6*,JUͱ͔ 3Y$PDPBʹґଘ͍ͨ͠ // ૂͬͨ Struct/Class/ఆ਺/enum ͷΈΛ import Ͱ͖Δ import struct

    RxCocoa.Signal import class UIKit.UIColor import var Photos.PHImageManagerMaximumSize 34
  14. 7JFX.PEFM͔ΒϦιʔεΛ ݟ͍ͨ // ViewModel public struct Outputs { // View

    ʹ͸৭Λ௨஌͍͕ͨ͠ɺ৭Ϧιʔε͸ App Ͱఆٛ͞Ε͍ͯΔ let backgroundColor: Driver<UIColor> } // ϓϩτίϧΛఆٛͯ͠৭ΛऔಘͰ͖ΔΑ͏ʹ͢Δ public protocol ColoConvertable { var color: UIColor { get } } // ৭ͷ৘ใʹม׵Λ͍ͨ͠σʔλͷ extension ͷதͰ ColorConvertable Λ࣮૷͍ͯ͠Δ // ͱ͖͚ͩਖ਼͘͠ಈ࡞͢ΔΑ͏ʹ͢Δ extension ItemError { var color: UIColor { guard let converter = self as? ColorConvertable else { fatalError("ColorConvertable Λ࣮૷͍ͯͩ͘͠͞") } return converter.color } } func bind(_ inputs: Inputs) -> Outputs { return .init(backgroundColor: itemModel .itemError .map { $0.color } // ColorConvertable Λ࢖ͬͯ৭৘ใΛͱΓͩ͢ .asDriver() ) } 36
  15. 7JFX.PEFM͔ΒϦιʔεΛ ݟ͍ͨ // ViewController // View ଆͰ͸ϦιʔεʹΞΫηε͕Ͱ͖ΔͷͰɺ৭ͷ৘ใΛฦ͢͜ͱ͕Ͱ͖Δ extension ItemError :

    ColorConvertable { var color: UIColor { switch { case .noItem: return .crimsonFireRed } } } func viewDidAppear() { // ViewModel ͱ bind ͢Δஈ֊Ͱ͸ͨͩ৭ͷ৘ใΛηοτ͢Ε͹͍͍ outputs.backgroundColor.drive(rx[\.backgroundColor]).disposed(by:disposeBag) } 37
  16. 6*"MFSU$POUSPMMFSΛ ࢖͍͍ͨ let tapConfirmDelete = PublishRelay<()>() let inputs = Inputs(

    tapConfirmDelete: tapConfirmDelete.asSignal(), tapBottomDeleteButton: tapBottomDeleteButton.asSignal() ) let output = viewModel.bind(inputs: inputs) output.showDeleteConfirm.emit(onNext: { [weak self] _ in guard let self = self else { return } let alert = UIAlertController(title: "౤ߘΛ࡟আ͠·͔͢ʁ", message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Ωϟϯηϧ", style: .cancel, handler: nil)) alert.addAction(UIAlertAction(title: "࡟আ", style: .destructive) { _ in tapConfirmDelete.accept(()) alert.dismiss(animated: true, completion: nil) }) self.present(alert, animated: true, completion: nil) }).disposed(by: disposeBag) 39
  17. 7JFX.PEFM*OQVUTJOJU
 ͕QVCMJD͡Όͳ͍໰୊ public class HogeViewModel { // Struct ͳͷͰ init

    ͕ࣗಈੜ੒͞ΕΔ͕ // σϑΥϧτͷΞΫηεम০ࢠͱͳΔͷͰɺ Framework ͷҧ͏ // View ͔Β͸ init Λݺͼग़͢͜ͱ͕Ͱ͖ͳ͍ public struct Inputs { public let tapButton: Single<()> public let inputText: Single<String> public let viewDidLoad: Signal<()> } 40
  18. 7JFX.PEFM*OQVUTJOJU
 ͕QVCMJD͡Όͳ͍໰୊ public class HogeViewModel { public struct Inputs {

    public let tapButton: Single<()> public let inputText: Single<String> public let viewDidLoad: Signal<()> // ͦΕͳΒࣗ෼Ͱॻ͔͘͠ͳ͍͡Όͳ͍ public init( // ςετίʔυͰ༨ܭͳมߋΛ࢈·ͳ͍ͨΊʹɺ // σϑΥϧτͰ never Λ͚ͭΔΑ͏ʹͳͬͨ tapButton: Signal<()> = .never(), inputText: Signal<String> = .never(), viewDidLoad: Signal<()> = .never() ) { self.tapButton = tapButton self.inputText = inputText self.viewDidLoad = viewDidLoad } } 41