Pro Yearly is on sale from $80 to $50! »

MVVMをベースに複雑な振る舞いを しっかり把握できるアプリ開発

MVVMをベースに複雑な振る舞いを しっかり把握できるアプリ開発

Realm meetup #6 で発表した
Sync iOS開発の舞台裏についてです
プロジェクトの話や、MVVM、ViewBindingなど多義にわたり解説しています

Acbf3391de0494432a92221ffe89f34e?s=128

yohei sugigami

August 26, 2015
Tweet

Transcript

  1. MVVMΛϕʔεʹෳࡶͳৼΔ෣͍Λ ͔ͬ͠Γ೺ѲͰ͖ΔΞϓϦ։ൃ yohei SUGIGAMI 10/21 shibuya.swift

  2. None
  3. Wantedly

  4. Sync Messenger 8FC %FTLUPQ"QQ "OESPJE BOEJ04 ϏδωείϛϡχέʔγϣϯಛԽϝοηʔδϯάαʔϏε

  5. Sync iOS 4XJGU .77. '31 1SPNJTF
 Ϟμϯͳཁૉٕज़

  6. Let’s Try TZOD Ұ൪࠷ॳͷݕࡧ݁Ռʹग़Δ͸ͣʂ

  7. Projects Summary

  8. Service Architecture $MJFOU$SPTT1MBUGPSN 4FSWFS.JDSPTFSWJDFT

  9. Team  "1* 8FC%FTLUPQ J04 "OESPJE GBTUMBOF

  10. iOS Schedule ݄̑ ݄̒ ݄̓ ϓϩτλΠϓ։ൃ 'JSFCBTFબఆ +0*/ ਃ੥ νʔϜ಺4MBDLஔ͖׵͑

    ࣾ಺4MBDLஔ͖׵͑ +0*/ ຊ։ൃ ࣾ֎ϢʔβϑΟʔυόοΫ ϲ݄ɺ̒ਓ݄͙Β͍ +0*/
  11. High Speed Development  13$MPTFE  *TTVFT$MPTFE ݄೔࣌఺  $PNNJU

  12. Review ϨϏϡʔΞʔΞαΠϯ 5FTU͕͚ͯ͜Δͷ͸ $JSDMF$*ͷௐࢠ͕ѱ͍ɻɻ

  13. Review Ϟνϕʔγϣϯ61Ͱ ੜ࢈ੑ޲্

  14. Delivery by fastlane 1VMM 3FR .FSHF .BTUFS %FMJWFSZ
 %FW5FTUFS .BTUFSʹ.FSHF͢Δ౓ʹλΠϜϦʔͳΞϓϦ഑෍

    ͡Ζ͏͘Μۘ੡ͷGBTUpMF
  15. Dog feeding ։ൃ̎िؒͰ4MBDL͔Β৐Γ׵͑Δ ։ൃऀͰ͋Δࣗ෼͔ͨͪΒ·ͣϑΝϯʹͳΔ ։ൃ̍ϲ݄Ͱࣾ಺Ͱ࢖ͬͯ΋Β͍ϑΟʔυόοΫΛ΋Β͏
 ։ൃ̎ϲ݄Ͱ਎ۙͳࣾ֎ͷํʑʹ΋࢖ͬͯ΋Β͍͞Βʹ
 ଟ͘ͷϑΟʔυόοΫΛ௖͖·ͨ͠

  16. Very Slow Swift Compile ̍ਓ.BD#PPL1SP̎୆Ͱ։ൃ ίϯύΠϧ଴ͪ࣌ؒΛ༗ޮ׆༻ ܕਪ࿦ͱΫϩδϟʔΩϟϓνϟ͕஗ͦ͏

  17. Very Slow Swift Compile .BD#PPL1SP JODI -BUF ͰϑϧίϯύΠϧ࣌ؒ ˠɹ̐෼ඵ
 ίʔυΛݟ௚ͯ͠ίϯύΠϧߴ଎Խ


    ˠɹ෼ඵʢඵʣ ˠɹࠩ෼ίϯύΠϧͷՄೳੑ΋Ξοϓ  ܕਪ࿦͠ͳ͍ͰܕΛ໌ه͢Δʢ"SSBZ΍%JDUJPOBSZ΋ʣ  ෳ਺ͷTXJGUϑΝΠϧΛҰͭʹ·ͱΊΔ  ܧঝ͠ͳ͍DMBTT͸pOBMΛ໌ه͢Δ  $BSUBHFରԠͷϥΠϒϥϦ͸1PETͰΠϯετʔϧ͠ͳ͍
  18. App Architecture

  19. MVVM (෩) $POUSPMMFS 7JFX.PEFM .PEFM 3FBDU,JU 4XJGU5BTL 4XJGUZ+40/ 4XJGU#POE 3FBMN

    3BX%BUB 1FSTJTUFOU 4UBUF.BOBHFNFOU -PHJD 7JFX%BUB 6*,JU%FMFHBUF #JOEJOH -JCSBSZ 3FTQPOTJCJMJUZ $SFBUF7JFX 4%8FC*NBHF 7JFX7JFX -BZFS
  20. ViewDataBinding 4UBUF 7JFX $POUSPMMFS 7JFX.PEFM %BUB ੹຿ͱؔ৺ࣄͷ෼཭ -PHJD 7JFX 7JFX.PEFMͷ-PHJD͸

    4UBUF %BUBͷมߋ͕੹຿ 7JFXʹ͍ͭͯ͸ؔ஌͠ͳ͍ PS #JOE 7JFX͸4UBUF %BUB͕มߋ
 ͞ΕͨΒಈతʹը໘Λߋ৽ %BUB 4UBUFͷϓϩηε͸ؔ஌͠ͳ͍ 7JFX #JOEJOH .FUIPE$BMM
  21. Instane Relation 7JFX7JFX $POUSPMMFS 7JFX.PEFM .PEFM ʜ ʜ 3FGFSFODF #VTJOFTT-PHJD

    4VC-PHJD 4IBSF-PHJD 4VC-PHJD %BUB %BUB %BUB %BUB %BUB %BUB .BQQJOH #JOEJOH 7JFX #JOEJOH 1SFTFOUBUJPO-PHJD 7JFX.PEFM͸ଞͷϨΠϠʔͷࢀরΛ࣋ͨͳ͍
  22. Request Data Flow Sequence 4XJGUZ+40/ 3FBMN 4XJGU5BTL 4XJGU#POE 1FSTJTUFODF 7JFX#JOEJOH

    "1*
  23. Request ViewBinding 4UBUF3FRVFTUJOH 4UBUF&SSPS JUFNT<> 4UBUF/POF *OEJDBUPS7JFX 3FUSZ7JFX /P%BUB7JFX

  24. Request ViewBinding 4UBUF3FRVFTUJOH 4UBUF&SSPS JUFNT<JUFN JUFN JUFN ʜ> 4UBUF/POF *OEJDBUPS7JFX

    3FUSZ7JFX /P%BUB7JFX
  25. Request ViewBinding final class RequestListViewModel<T: Identifier> { let items: DynamicArray<T>

    = DynamicArray([]) var requestListState = Dynamic<RequestListState>(.None) var noDataFirstViewHidden: Dynamic<Bool> { let a = indicatorViewHidden.map { $0 == false } let b = requestListFirstState.map { $0 == .Error } return reduce(a, b, c) { $0 || $1 == true } } var indicatorViewHidden: Dynamic<Bool> { let a = requestListFirstState.map { $0 != RequestListState.Requesting } let b = items.map { count($0) > 0 } return reduce(a, b) { $0 || $1 == true } } var retryViewHidden: Dynamic<Bool> { let a = requestListFirstState.map { $0 != RequestListState.Error } let b = items.map { count($0) > 0 } return reduce(a, b) { $0 || $1 == true } } 3FRVFTU4UBUFͱ*UFNTͷঢ়ଶͷΑΔڍಈΛએݴ

  26. Request ViewBinding final class ContactsViewController: UIViewController { var tableViewDataSourceBond: UITableViewDataSourceBond<ContactCell>!

    let viewModel = ContactsViewModel() let indicatorView = InstantiateFromNib(IndicatorView) let retryView = InstantiateFromNib(RetryView) let noDataView = InstantiateFromNib(NoDataView) override func viewDidLoad() { super.viewDidLoad() viewModel.requestList.indicatorViewHidden ->> indicatorView.dynHidden viewModel.requestList.retryViewHidden ->> retryView.dynHidden viewModel.requestList.noDataFirstViewHidden ->> noDataView.dynHidden } 7JFX$POUSPMMFSͰ7JFX.PEFMͷ4XJGU#POEͱ7JFXΛ#JOEJOH
  27. Request ViewBinding final class RequestListViewModel<T: Identifier> { typealias RequestTask =

    Task<Progress, ResponseCollection<T>, NSError> func requestFirst(task: RequestTask) -> RequestTask { self.requestListState.value = .Requesting task.success { [weak self] (collection: ResponseCollection<T>) -> Void in self?.items.setArray(collection.items) self?.requestListState.value = .None }.failure { [weak self] (errorInfo: RequestTask.ErrorInfo) -> Void in self?.requestListState.value = .Error } return task } 3FRVFTUͷ։࢝ɺਖ਼ৗ׬ྃɺҎ্׬ྃͰ4BUFΛߋ৽ ਖ਼ৗ׬ྃ࣌ʹJUFNTΛߋ৽
  28. Request ViewBinding final class RequestListViewModel<T: Identifier> { lazy var stateChangedObserver

    = Bond<RequestListState> { [weak self] state in switch state { case .Requesting: UIApplication.sharedApplication().networkActivityIndicatorVisible = true default: UIApplication.sharedApplication().networkActivityIndicatorVisible = false } } init() { requestListState ->| stateChangedObserver } OFUXPSL"DUJWJUZ*OEJDBUPS7JTJCMFΛ4UBUFͰ#JOEJOH
  29. Input with Validation ViewBinding &OBCMF4JHO6Q#VUUPO
 7BMJEBUJPO&NBJM1BTTXPSE

  30. Validation ViewBinding final class SignUpViewModel { let email = Dynamic<String>("")

    let password = Dynamic<String>("") var emailError: Dynamic<NSError?> { return email.map { Validator.Email($0).validate() } } var passwordError: Dynamic<NSError?> { return password.map { Validator.Password($0).validate() } } var isSignInInput: Dynamic<Bool> { let isEmailValid = emailError.map { nil == $0 } let isPasswordValid = passwordError.map { nil == $0 } return reduce(isEmailValid, isPasswordValid) { $0 && $1 == true } } &NBJMͱ1BTTXPSEͷ7BMJEBUJPOʹΑͬͯ 4JHO6Q͕&OBCMFʹͳΔ͔Λએݴ

  31. Validation ViewBinding final class SignUpViewController { override func viewDidLoad() {

    super.viewDidLoad() emailTextField ->> viewModel.email passwordTextField ->> viewModel.password viewModel.signUpState ->| signUpStateChangedObserver viewModel.isSignInInput ->> signUpButton.dynEnabled 7JFX$POUSPMMFSͰ7JFX.PEFMͷ4XJGU#POEͱ7JFXΛ#JOEJOH enum Validator { case Email(String?) case Password(String?) func validate() -> NSError? { switch self { case .Email(var value): return NGRValidator.validateValue(value, named: LocalizedString("Email")) { (validator: NGRPropertyValidator!) in validator.required().msgNil(LocalizedString("is required.")) validator.syntax(NGRSyntax.Email).msgWrongSyntax(NGRSyntax.Email, LocalizedString("is not valid syntax.")) } case .Password(var value): return NGRValidator.validateValue(value, named: LocalizedString("Password")) { (validator: NGRPropertyValidator!) in validator.required().msgNil(LocalizedString("is required.")) validator.minLength(6).msgTooShort(LocalizedString("is too short.")) }
  32. No inherits 7JFX$POUSPMMFS 7JFX 7JFX.PEFM .PEFMʹ͓͍ͯ "CTUSBDUϨΠϠʔΛઃ͚ͳ͍ʢܧঝ͠ͳ͍ʣ ܧঝ͢Δͱɺɺɺ  ϩδοΫ΍ڍಈΛཧղʹ͠ʹ͍͘

     ڞ௨Խͨ͠ίʔυΛมߋ͠ʹ͍͘ SFGT J04ΞϓϦͷઃܭͰ#BTF7JFX$POUSPMMFSͷΑ͏ͳͷ͸࡞Γͨ͘ͳ͍CZࠓ৓ઌੜ
 IUUQRJJUBDPNZJNBKPJUFNTFGFCECGE
  33. No inherits  %FMFHBUF  (FOFSJDT  .PEFMΛ೚ҙͷ1SPUPDPMΛ४ڌͤͯ͞
 (FOFSJDTͰϩδοΫΛڞ௨Խ 

    "TQFDU  ϩΪϯά΍"OBMZUJDTૹ৴  $MBTT  ڞ௨ॲཧΛ$MBTTͱͯ͠੾Γग़͠ ڞ௨Խ͍ͨ͠ίʔυ͸ҎԼͷ׆༻Λݕ౼͢Δ
  34. Qiitaʹ΋ॻ͖·ͨ͠ IUUQRJJUBDPNTVTJFZZJUFNTBGCCEGG

  35. Enjoy Sync life.

  36. iOS, Android Rails ΤϯδχΞ ืूத

  37. MVVM .7$%JBHSBN 7JFX$POUSPMMFS 7JFX .PEFM 7JFX$POUSPMMFSJTlEPFTBMMUIFUIJOHTz

  38. MVVM .77.%JBHSBN 7JFX 7JFX$POUSPMMFS 7JFX.PEFM .PEFM 7JFX$POUSPMMFSCFDPNFTNBMMFS 7JFXJTTUBUFMFTT