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

大規模なアプリのマルチモジュール構成の実践

011714704c4a925e542d426d4cdaa4e3?s=47 giginet
September 19, 2021

 大規模なアプリのマルチモジュール構成の実践

011714704c4a925e542d426d4cdaa4e3?s=128

giginet

September 19, 2021
Tweet

Transcript

  1. ࣗݾ঺հ • @giginet • iOSςοΫϦʔυ@ Cookpad • ίϛολʔ fastlane/XcodeGen/CarthageͳͲ •

    ٕज़ސ໰ @ taskey, MoneyForward
  2. None
  3. େن໛ͳΞϓϦͷϚϧνϞ δϡʔϧߏ੒ͷ࣮ફ iOSDC 2021 9/19 Track A @giginet iOSDC 2021

  4. ͜ͷτʔΫʹ͍ͭͯ • ୈ1ষɿ2019೥ࠒ͔ΒΫοΫύουϨγϐΞϓϦͷϚϧνϞδϡʔϧ ԽΛਐΊ͖ͯͯ͠͹Β͘ܦͬͨͷͰݱঢ়Λ঺հ • ୈ2ষɿଞͷϓϩδΣΫτ͕େن໛ͳϦΞʔΩςΫνϟΛ࣮ફ͢Δʹ ͸Ͳ͏ͨ͠ΒΑ͍͔

  5. 5 View Model ViewController 🔥ϩδοΫΛ࣋ͭ7JFX XXXManager 🔥γϯάϧτϯͷσʔλιʔε 🔥.BTTJWF7JFX$POUSPMMFS 🔥ڞ௨Խ͞ΕͯංେԽͨ͠Ϟσϧ ☠

    🔥σουίʔυ 🔥ີ݁߹ 🔥7JFXͷܧঝɺ࢖͍ճ͠ 🈲 🔥৮ͬͯ͸ ͍͚ͳ͍΍ͭ 🌀 🔥ࠞಱ ϚϧνϞδϡʔϧԽҎલ
  6. ΫοΫύουΞϓϦ • 50 Releases / year • 15 developers /

    release • 40 ~ 50 PR / release • 328,000 lines
  7. 2019೥ࠒͷঢ়گͱ໨త • ։ൃ࣌ͷϏϧυ଴ͪΛݮΒ͍ͨ͠ • ࠩ෼Ϗϧυʹ΋1ʙ2෼ͷ଴ͪ࣌ؒ • ػೳؒͷґଘ΍෭࡞༻ΛݮΒ͍ͨ͠ • ςετهड़Λ؆ུԽ͍ͨ͠ •

    ObjCͳͲͷݹ͍࣮૷ΛӅṭ͍ͨ͠
  8. None
  9. • https://fortee.jp/iosdc-japan-2021/proposal/6b6fb68b-a45a-4878-a8b9-0dd5ed90fc39 ΫοΫύουͷΤϯδχΞ͕ޠΔɺڊେͰྺ࢙͋ΔΞϓϦʹ͓͚Δഁ յͱ૑଄ - ϩάϛʔTech https://logmi.jp/tech/articles/321186

  10. ΞʔΩςΫνϟ • ΫϦʔϯΞʔΩςΫνϟ͸͋Δఔ౓ಋೖࡁΈʢಋೖ͞Ε͍ͯͳ͍ͱ͜ Ζ΋͋ͬͨʣ • VIPERΛ࠾༻ɺΞϓϦέʔγϣϯ૚ͱυϝΠϯ૚ʹ෼͔ΕΔ • ֤ը໘ΛVIPERγʔϯͱݺͿ

  11. None
  12. Feature A Feature B Feature C 7*1&3γʔϯ

  13. ݱࡏͷΫοΫύουΞϓϦ

  14. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥
  15. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥 $PSFϞδϡʔϧ ந৅ԽͷͨΊͷΠϯλʔϑΣΠε ɾΞϓϦ಺Ͱڞ௨ͯ͠࢖͏6*ίϯϙʔ ωϯτͷఏڙ
  16. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥 'FBUVSF.PEVMF ڞ௨ͷυϝΠϯ૚Λ͍͔࣋ͭͭ͘ͷ 7*1&3γʔϯΛଋͶͨϞδϡʔϧ
  17. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥 7*1&3γʔϯ 'FBUVSF.PEVMF
  18. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥 ΞϓϦέʔγϣϯ "QQMJDBUJPO5BSHFUʹ૬౰ શͯͷґଘΛ஌ΕΔ
  19. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥 $PPLQBE 7*1&3Խ͞Ε͍ͯΔ͕ϞδϡʔϧԽ͞ Ε͍ͯͳ͍γʔϯͷू߹
  20. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥 5TVLVSV ࠞಱ
  21. CookpadCore Cookpad ɾɾɾ Cookpad Tsukuru Feature A Feature B Feature

    C 🔥
  22. Feature A Feature B ❌ Feature Moduleؒ͸ґଘΛ࣋ͯͳ͍ ॥؀ࢀরΛආ͚ΔͨΊ Feature Aͷ࣋ͭγʔϯ͔ΒFeature

    Bͷ࣋ͭ γʔϯʹભҠ͍ͨ͠৔߹ͳͲʹࠔΔ
  23. CookpadCore Feature A Feature B Environment Cookpad ᶃ&OWJSPONFOU΁ͷ໰͍߹Θͤ ᶄґଘͷ஫ೖ ᶅґଘͷऔಘ

  24. Environment • શͯͷґଘΛ࣋ͭProtocol • CoreϞδϡʔϧʹ͍Δʢ=͢΂͔ͯΒࢀরՄೳʣ • ΞϓϦέʔγϣϯλʔήοτͰ࣮૷ͯ͠ґଘΛ஫ೖ • ֤γʔϯ͸ੜ੒࣌ʹEnvironmentΛ΋Β͏ •

    ෭࡞༻΍ґଘʹ͸Environmentܦ༝Ͱ৮Δ͜ͱͰɺ؀ڥʹΑͬͯڍಈ Λࠩ͠ସ͑Δ͜ͱ͕Ͱ͖Δ
  25. import Foundation import UIKit public protocol Environment { var client:

    ServiceClient { get } var pvLogger: PVLogger { get } var userFeatures: UserFeatures { get } var activityLogger: ActivityLogger { get } // … } CookpadCore ґଘΛQSPUPDPMԽ
  26. final class CookpadEnvironment: Environment { var client: ActivityLogger { CookpadActivityLogger(

    ) } // … } Cookpad ΞϓϦέʔγϣϯλʔήοτͰ ۩ମతͳ࣮૷Λฦ͢
  27. environment.activityLogger.post(eventName: “tap_button”) Feature A 'FBUVSF.PEVMF͔Β FOWJSPONFOUܦ༝Ͱར༻Ͱ͖Δ

  28. 28 Environment CookpadEnvironment StubbableEnvironment Cookpad Tests Network Logger ը໘ભҠ

  29. let viewController = RecipeDetails.RecipeDetailsViewController(recipeID: 42 ) present(viewController, animated: true, completion:

    nil ) Feature A 3FDJQF%FUBJMTϞδϡʔϧ಺ͷ γʔϯ͸'FBUVSF"Ϟδϡʔϧ͔ Β͸ࢀরͰ͖ͳ͍ ❌
  30. DescriptorͱResolver • Environment͕࣋ͭґଘղܾͷ࢓૊Έ • Descriptor͸ͦͷ࣮૷Λࣔ͢ϚʔΧʔ • CoreϞδϡʔϧʹ͍Δɻʢ=͢΂͔ͯΒࢀরՄೳʣ • EnvironmentʹDescriptorΛ౉ͯ͠ɺܕফڈ͞Ε࣮ͨଶΛ΋Βͬͯ͘ Δ࢓૊Έ

    = ResolverͱݺΜͰ͍Δ
  31. import Foundation import UIKit public protocol Environment { var client:

    ServiceClient { get } var pvLogger: PVLogger { get } var userFeatures: UserFeatures { get } var activityLogger: ActivityLogger { get } // … func resolve<Descriptor: TypedDescriptor>(_ descriptor: Descriptor) -> Descriptor.Outpu t } CookpadCore 3FTPMWFS %FTDSJQUPSΛड͚औͬͯ ࣮૷Λฦ͢ϝιου %FTDSJQUPS ࣮૷Λࢦࣔ͢͠ϚʔΧʔ
  32. public struct RecipeDetailsDescriptor: TypedDescriptor { public typealias Output = UIViewControlle

    r public var recipeID: Int6 4 public init(recipeID: Int64) { self.recipeID = recipeI D } } CookpadCore %FTDSJQUPS ࣮૷Λࢦࣔ͢͠ϚʔΧʔ
  33. final class CookpadEnvironment: Environment { public func resolve<Descriptor: TypedDescriptor>(_ descriptor:

    Descriptor) -> Descriptor.Output { switch descriptor { case let d as ViewDescriptor.RecipeDetailsDescriptor : return RecipeDetailsViewBuilder.build(with: d, environment: self) as! Descriptor.Outpu t } } } Cookpad %FTDSJQUPS͝ͱʹ࣮૷Λฦͯ͠ ΞοϓΩϟετ͢Δ
  34. let viewController: UIViewController = environment.resolve(ViewDescriptor.RecipeDetailsDescriptor(recipeID: 42) ) viewController.present(viewController, animated: true,

    completion: nil ) Feature A 3FTPMWFSܦ༝ͰऔΓग़͢͜ͱͰɺผͷϞδϡʔϧ ಺ͷγʔϯ΋6*7JFX$POUSPMMFSͱͯ͠औಘͰ͖Δ
  35. None
  36. CookpadCore FeatureASandbox Feature A SandboxΞϓϦ

  37. SandboxΞϓϦ ΞϓϦશମ

  38. ػೳ͝ͱʹಈ࡞͢ΔϛχΞϓϦͰϓϨϏϡʔαΠΫϧΛര଎ʹͨ͠࿩ https://fortee.jp/iosdc-japan-2021/proposal/6b6fb68b-a45a-4878-a8b9-0dd5ed90fc39

  39. ϚϧνϞδϡʔϧԽͷޮՌ • ϛχΞϓϦ(Sandbox)ͷ੔උʹΑΔ։ൃମݧ(DX)ͷ޲্ • Feature ModuleؒͷӨڹΛݮΒͤͨ • ಛఆϞδϡʔϧ಺ͰͷΈͷSwiftUIಋೖͳͲɺϞδϡʔϧ಺Ͱͷ௅ઓ͕ ͠΍͘͢ͳͬͨ

  40. ·ͱΊ • Ϟδϡʔϧ෼཭։࢝લͷ໨తͷఆٛ͸ॏཁ • ໨త͕ఆ·͍ͬͯΕ͹ࣗͣͱ෼཭ํ๏͕ܾ·Δ • վળͷঢ়گΛఆྔԽͯ͠؂ࢹ • ґଘղܾͷ࢓૊Έ͸೉͍͠ •

    ࣮ࡍʹ෼཭͕ਐΜͩΒ͔֬ʹޮՌ͕͋ͬͨʢΑ͔ͬͨʣ
  41. ࣮ફతϚϧνϞδϡʔϧ

  42. Ϟδϡʔϧ෼཭Λਁಁͤ͞Δʹ͸ • શͯΛҰ౓ʹҠߦ͠ͳ͍ • ݀Λ࠹͙ • ։ൃऀ͕໎͏఺ΛݮΒ͢

  43. શͯΛҰ౓ʹҠߦ͠ͳ͍ ෦෼తͳҠߦΛҙࣝͨ͠ઃܭ

  44. ෦෼తͳҠߦΛҙࣝͨ͠ઃܭ • ݹ͍࣮૷ΛҠߦ͠ͳͯ͘΋৽ن࣮૷ͷΈΛϞδϡʔϧ෼཭Ͱ͖ΔΑ͏ ʹҙࣝͯ͠ઃܭ • protocolʹΑΔந৅Խ • protocolΛऔΓग़ͤΔResolver

  45. ྫ1ɿݹ͍γϯάϧτϯͷ࣮૷ͷར༻ • ݹ͍࣮૷͸protocolΛCoreϞδϡʔϧʹఆٛ͠ɺFeature Module͔ Β͸protocolͱͯ͠ར༻͢Δ • ྫ͑͹LegacyActivityLogger.sharedΛந৅Խͯ͠environment͔Βఏ ڙ͢Δ • ͜͏͢Δ͜ͱͰɺLegacyActivityLoggerΛมߋͤͣʹ֤γʔϯ͔Β͸

    ࣮૷ΛӅṭͰ͖Δ
  46. import Foundation public protocol ActivityLogger { func post(eventName: String, payload:

    [String: Any]? ) } public protocol Environment { var activityLogger: ActivityLogger { get } // … } CookpadCore
  47. import CookpadCor e private struct CookpadActivityLogger: ActivityLogger { func post(eventName:

    String, payload: [String: Any]?) { LegacyActivityLogger.shared.post(eventName: eventName, payload: payload ) } } final class CookpadEnvironment: Environment { var activityLogger: ActivityLogger { return CookpadActivityLogger( ) } } Cookpad ΞϓϦέʔγϣϯλʔήοτ͔ Βಈ͔ͤͳ͍ݹ͍࣮૷Λϥοϓ ͢Δ
  48. environment.activityLogger.post(eventName: “search” , payload: ["keyword": "όλʔείονγφϞϯύΠ"] ) Feature A 'FBUVSF.PEVMF͔Β

    FOWJSPONFOUܦ༝Ͱར༻Ͱ͖Δ
  49. ྫ2ɿݹ͍ViewController΁ͷભҠ • Resolverͷ࢓૊Έ͸ݹ͍࣮૷ͷӅṭʹ΋໾ཱͭ • ΫϦʔϯΞʔΩςΫνϟԽ͞Ε͍ͯͳ͍ݹ͍ը໘͕͋ͬͨͱͯ͠΋ɺ ResolverΛ࣮૷͢Ε͹৽͍࣮͠૷͔Βಉ͡Α͏ʹར༻Ͱ͖Δ • ݹ͍ը໘͸Ϟδϡʔϧ෼཭͠ͳͯ͘΋ɺΞϓϦέʔγϣϯλʔήοτ ͔Β஫ೖ͢Ε͹ྑ͍

  50. final class LegacyViewBuilder: ViewBuilder { func build(environment: Environment) -> UIViewController

    { // VIPERԽ͞Ε͍ͯͳ͍Objective-CͰ࣮૷͞Εͨݹ͍ViewController let legacyViewController = CKDLegacyViewController( ) return legacyViewControlle r } } Cookpad ΫϦʔϯΞʔΩςΫνϟԽ͞Ε͍ͯͳ͍ݹ͍ը໘΋ 7JFX#VJMEFSͰϥοϓ͢Δͱ3FTPMWFSܦ༝ͰऔಘͰ͖Δ
  51. ৽ن࣮૷͕࣮֬ʹҠߦ͞ΕΔΑ͏ʹ͢Δ ݀Λ࠹͙

  52. ৽ن࣮૷͕࣮֬ʹҠߦ͞ΕΔΑ͏ʹ͢ΔͨΊʹ • ݹ͍࣮૷͸Ұ୴ఘΊΔ • ৽͍࣮͠૷ʹ͍ͭͯίʔυੜ੒ػͳͲΛ४උ͠ɺ؆୯ʹϞδϡʔϧ͕ ੜ੒Ͱ͖ΔΑ͏ʹ͢Δ

  53. None
  54. None
  55. Ϟδϡʔϧͷੜ੒ • yonaskolb/XcodeGenΛར༻ • XcodeGenͷTarget TemplateػೳͰॊೈʹTargetΛ૿΍ͤΔ • XcodeGenͷProject SpecΛੜ੒͢Δ࢓૊Έ(XcodeGenGen)

  56. targetTemplates : FeatureModule : platform: iOS type: framework sources :

    - ${frameworkName} settings : base : PRODUCT_BUNDLE_IDENTIFIER: com.cookpad.${frameworkName} MACH_O_TYPE: staticlib info : path: ${frameworkName}/Info.plist dependencies : - sdk: Foundation.framework - sdk: UIKit.framework - target: CookpadCor e prebuildScripts : - name: SwiftLint path: "configs/scripts/swift_lint_module.sh" ม਺
  57. -- - targets : MyAwesomeModule : templates : - FeatureModule

    templateAttributes : frameworkName: MyAwesomeModule ม਺ ܧঝݩͷ ςϯϓϨʔτ
  58. γʔϯͷੜ੒ • yonaskolb/GenesisΛར༻ • ςϯϓϨʔτ͔Βίʔυੜ੒Λߦ͏ϢʔςΟϦςΟ • ؆୯ͳઃఆϑΝΠϧͰର࿩తͳΠϯλʔϑΣΠεΛ࡞ΕΔ

  59. import CookpadCore import UIKit public final class {{ name }}ViewController:

    UIViewController, {{ name }}ViewContract { private var presenter: {{ name }}PresenterContract ! private let environment: Environmen t init(environment: Environment) { self.environment = environmen t super.init(nibName: nil, bundle: nil ) } @available(*, unavailable ) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented" ) } func inject(presenter: {{ name }}PresenterContract) { self.presenter = presente r } override public func viewDidLoad() { super.viewDidLoad( ) // Insert code here to connect presenter } ม਺
  60. # name must be given as options to Genesis options

    : - name: name question: VIPER scene name? description: new VIPER scene name to generate(e.g. RecipeDetails). type: string required: true - name: module question: Destination target? description: Target destination name to generate new VIPER scene. (e.g. RecipeDetails) type: string required: true files : - template: VIPER/FeatureModule/Regular/Contract.stencil path: "{{ module }}/Application/{{ name }}/{{ name }}Contract.swift" - template: VIPER/FeatureModule/Regular/ViewController.stencil path: "{{ module }}/Application/{{ name }}/{{ name }}ViewController.swift" - template: VIPER/FeatureModule/Regular/Wireframe.stencil path: "{{ module }}/Application/{{ name }}/{{ name }}Wireframe.swift ” … ࣭໰ͷ౴͑Λม਺ʹ֨ೲͰ͖Δ ͲͷςϯϓϨʔτΛͲ͜ʹੜ੒͢Δ͔ࢦఆ
  61. ։ൃऀ͕໎͏఺ΛݮΒ͢ ৘ใͷڞ༗ͱؒҧ͑ͳ͍࢓૊ΈԽ

  62. ։ൃऀ͕໎͏఺ΛݮΒ͢ • υΩϡϝϯτ΍τϥϒϧγϡʔςΟϯάͳͲͷϦιʔεΛ༻ҙ͢Δ • มߋ͕͋ͬͨ৔߹௥ै͢Δ • มߋʹ͍ͭͯɺࣾ಺ϒϩάͳͲͰ௨஌͢Δ • ษڧձ΍࠲ஊձͳͲΛ௨ͯ͠՝୊Λ἞Έ্͛Δ •

    ೔ʑͷϨϏϡʔ
  63. None
  64. None
  65. ϕετϓϥΫςΟεͷఏڙ • ࣮૷࣌ʹఆ͕ٛ͋΍;΍ͱͳͬͯ͠·͏఺ʹ͍ͭͯɺݸผʹϕετϓ ϥΫςΟεΛఏڙ͢Δ • Ͳ͜ͷϞδϡʔϧʹ΋ஔ͚Δ࣮૷ΛͲ͜ʹ͓͘΂͖͔ • ༷ʑͳґଘղܾͷखஈ͕͋Δͱ͖ʹͲͷΑ͏ͳखஈΛ࠾༻͢΂͖͔

  66. ࢓૊ΈʹΑΔϦΞʔΩςΫνϟͷਪਐ • ΠϯηϯςΟϒͷઃܭ • Ϟδϡʔϧ෼཭͕ਐΉͱ୯ମϏϧυ͕ૣ͘ͳͬͯ։ൃ͠΍͘͢ͳΔ ͱݴͬͨϝϦοτΛଧͪग़͢ • δΣωϨʔλ΍ϝλϓϩάϥϛϯά • ੩తղੳ

  67. ϝλϓϩάϥϛϯάͷར༻ • ίʔυੜ੒ʢϝλϓϩάϥϛϯάʣΛར༻Ͱ͖ΔϲॴͰ͸ར༻͢Δ • ࣮૷ͷ܁Γฦ͕͠ආ͚ΒΕɺҠߦ͕͠΍͘͢ͳΔ • ఆܕ͔Β֎Ε࣮ͨ૷͕Ͱ͖ͳ͘ͳͬͨΓ • ࣮૷๨ΕΛίϯύΠϧ࣌ʹݕग़Ͱ͖ΔΑ͏ʹͰ͖Δ

  68. ྫ1: Resolverͷੜ੒ࣗಈԽ • ઌ΄ͲݟͨDescriptor͔ΒViewControllerΛresolve͢Δ࣮૷Λϝλϓ ϩάϥϛϯάʹΑͬͯࣗಈԽ • ࣅͨΑ͏ͳίʔυͷ܁Γฦ͠Λຖճ࣮૷͠ͳͯ͘ྑ͘ͳͬͨ

  69. final class CookpadEnvironment: Environment { public func resolve<Descriptor: TypedDescriptor>(_ descriptor:

    Descriptor) -> Descriptor.Output { switch descriptor { case let d as ViewDescriptor.RecipeDetailsDescriptor : return RecipeDetailsViewBuilder.build(with: d, environment: self) as! Descriptor.Outpu t } } } Cookpad ࣮૷๨Ε͕͋ΔͱΫϥογϡ͢Δ ୯७ʹຖճ࣮૷͢Δͷ͕໘౗ ͜͜ʹෳࡶͳॲཧ͕ॻ͚ͯ͠·͏ ❌ ❌ ❌
  70. // ࣗಈੜ੒͢ΔΑ͏ʹͨ͠ final class CookpadEnvironment: Environment { public func resolve<Descriptor:

    TypedDescriptor>(_ descriptor: Descriptor) -> Descriptor.Output { switch descriptor { case let d as ViewDescriptor.RecipeDetailsDescriptor : return RecipeDetailsViewBuilder.build(with: d, environment: self) as! Descriptor.Outpu t } } } Cookpad શέʔε͕໢ཏ͞ΕΔΑ͏ʹ ຖճ࣮૷͠ͳͯ͘ྑ͍ ఆܗ֎ͷ࣮૷͕ෆՄೳʹ
  71. ͜ͷ࢓૊Έͷར఺ • ໢ཏੑͷ୲อ • શDescriptorͷResolver͕͋Δ͜ͱΛίϯύΠϧ࣌ʹอূͰ͖Δ • ن໿΁ͷద߹ͷอূ • ResolverͷதͰҙਤ͠ͳ͍ڍಈΛ࣮૷͢ΔͳͲ͕Ͱ͖ͳ͘ͳΓɺΠϯ λʔϑΣΠε͕ἧ͑ΒΕΔ

    • লྗԽ • ։ൃऀ͸DescriptorͱViewBuilderΛ༻ҙ͢Δ͚ͩͰྑ͍
  72. ίʔυੜ੒Λ༻͍ͨiOSΞϓϦϚϧνϞδϡʔϧԽͷͨΊͷґଘղܾ https://techlife.cookpad.com/entry/2021/06/16/110000

  73. ྫ2: ϞοΫͷࣗಈੜ੒ • CoreϞδϡʔϧʹ͍ΔProtocolͷϞοΫΛࣗಈੜ੒͢Δ • Ϣχοτςετ΍SandboxΞϓϦͰμϛʔͷ஋Λྲྀ͠ࠐΈ͍ͨͱ͖ʹ ༗༻ • krzysztofzablocki/SourceryͰ࣮ݱ͍ͯ͠Δ •

    ࠷ۙ͸uber/mockoloͱ͍͏΍͕ͭ͋Γஔ͖׵͑ΒΕͦ͏
  74. protocol RecipeSearchDataSource { func searchRecipe(keyword: String) -> Single<[Recipe]> } CookpadCore

  75. class MockRecipeSearchDataSource: RecipeSearchDataSource { lazy var searchRecipeReturnValue: Single<[Recipe]> = {

    fatalError("\ (#line)") }( ) // MARK: - searchRecipe var searchRecipeCallsCount = 0 var searchRecipeCalled: Bool { return buildCallsCount > 0 } func searchRecipe(keyword: String) -> Single<[Recipe]> { searchRecipeCallsCount += 1 return searchRecipeReturnValu e } } let dataStore = MockRecipeSearchDataSource( ) dataStore.searchRecipeReturnValue = .just([recipe0, recipe1] ) dataStore.searchRecipe(keyword: “φΠεΫϦʔϜ” )
  76. ͦͷଞͷ޻෉ • Interface Builderͷ੩తղੳΛߦ͏ • IBDecodable/IBLinter • ϞδϡʔϧҠߦͷͨΊͷεΫϦϓτ੔උ

  77. Ϟδϡʔϧ෼཭ͷਁಁͷͨΊʹͰ͖Δ͜ͱ • ͍ͬ΃ΜʹҠߦ͢Δͷ͸೉͍͠ • ৽͍࣮͠૷ͷΈʹ͍ͭͯҠߦ͠΍͍͢Α͏ʹ͢Δ • ݹ͍࣮૷Λޙճ͠ʹͰ͖Δઃܭ • ϝϯόʔͷڠྗΛऔΓ෇͚Δ޻෉ •

    ਓखΛ࢖Θͳͯ͘΋Ҡߦ͕ਐΉ࢓૊Έ࡞Γ • ίʔυੜ੒ɺlinterɺ ੩తղੳ
  78. ·ͱΊ • ΫοΫύουͰͷϚϧνϞδϡʔϧԽͷ͜Ε·Ͱͱݱঢ় • ଞͷϓϩδΣΫτ΁ͷదԠͷҰॿͱͳΕ͹޾͍Ͱ͢ • աڈͷࢿྉΛࢀর௖͍ͨΓɺask the speakerͰ࿩͠·͠ΐ͏ •

    Twitter : @giginet ·Ͱ͓ؾܰʹ
  79. ࢀߟࢿྉ • ʙբ͕ؔʙ ΫοΫύουiOSΞϓϦͷഁյͱ૑଄ɺͦͯ͠ະདྷ • https://techconf.cookpad.com/2019/kohki_miki.html • ΫοΫύουͷΤϯδχΞ͕ޠΔɺڊେͰྺ࢙͋ΔΞϓϦʹ͓͚Δഁ յͱ૑଄ -

    ϩάϛʔTech • https://logmi.jp/tech/articles/321186
  80. • XcodeGenʹΑΔ৽࣌୅ͷiOSϓϩδΣΫτ؅ཧ • https://techlife.cookpad.com/entry/2019/04/26/110000 • େن໛ͳiOSΞϓϦͷը໘։ൃΛޮ཰Խ͢ΔͨΊʹಈ࡞֬ೝ༻ϛχΞ ϓϦΛߏங͢Δ • https://techlife.cookpad.com/entry/2020/08/05/090000 •

    ίʔυੜ੒Λ༻͍ͨiOSΞϓϦϚϧνϞδϡʔϧԽͷͨΊͷґଘղܾ • https://techlife.cookpad.com/entry/2021/06/16/110000
  81. • 5/26 Cookpad Lounge #3 ʮΫοΫύου iOS ΞϓϦΛര଎Ͱ։ൃͰ ͖ΔΑ͏ʹ͢Δ࿩ʯ •

    https://www.youtube.com/watch?v=hqWiOeRyZ94 • 6/16 iOS Tech Talk ʙ Multi module ઓུ࠲ஊձ ʙ • https://www.youtube.com/watch?v=5p6h5yiQ2PQ • 7/2 iOS Tech Talk ʙ Multi module ઓུ࠲ஊձ vol.2 • https://www.youtube.com/watch?v=glpfnnDDaz8
  82. એ఻ • After Party iOSDC Japan 2021 • https://cookpad.connpass.com/event/222056/

  83. ͝ਗ਼ௌ ͋Γ͕ͱ͏͍͟͝·ͨ͠ @giginet