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

機能ごとに動作するミニアプリでプレビューサイクルを爆速にした話

aoi
September 18, 2021

 機能ごとに動作するミニアプリでプレビューサイクルを爆速にした話

aoi

September 18, 2021
Tweet

More Decks by aoi

Other Decks in Technology

Transcript

  1. ػೳ͝ͱʹಈ࡞͢ΔϛχΞϓϦͰ


    ϓϨϏϡʔαΠΫϧΛര଎ʹͨ͠࿩
    iOSDC JAPAN 2021 2021/09/18 Track C @aomathwift

    View Slide

  2. ࣗݾ঺հ
    • Aoi Okawa @aomathwift


    • Cookpad Inc. iOS App Developer

    View Slide

  3. ։ൃதͷը໘Ͳ͏΍ͬͯ֬ೝͯ͠·͔͢ʁ

    View Slide

  4. શ෦Ϗϧυͯ͠ؤுΔ
    • ͕͔͔࣌ؒΔ


    • ֬ೝ͍ͨ͠ը໘ʹ͙ͨ͢ͲΓ͚ͭͳ͍


    • ։ൃ్தͰಋઢ͕ͳ͍

    View Slide

  5. શ෦Ϗϧυͯ͠ؤுΔ
    • ͕͔͔࣌ؒΔ


    • ֬ೝ͍ͨ͠ը໘ʹ͙ͨ͢ͲΓ͚ͭͳ͍


    • ։ൃ్தͰಋઢ͕ͳ͍


    • ͕͔͔࣌ؒΔ
    ޮ཰ΘΔ͍😇

    View Slide

  6. ϑϧϏϧυ͢ΔͷΛ΍ΊΑ͏💡

    View Slide

  7. ϛχΞϓϦͷಋೖ

    View Slide

  8. ର৅
    • ΞϓϦಈ࡞֬ೝͷͨΊʹ௕͍Ϗϧυ࣌ؒΛ଴͍ͬͯΔਓ


    • ։ൃաఔͷϓϨϏϡʔαΠΫϧΛվળ͍ͨ͠ਓ

    View Slide

  9. Agenda
    • ΫοΫύουiOSΞϓϦͰͷϛχΞϓϦӡ༻ࣄྫ


    ‣ ୯ମىಈՄೳͳϞδϡʔϧߏ੒


    ‣ ϛχΞϓϦͷ࢓૊Έ


    ‣ νʔϜશମͰར༻ͯ͠΋Β͏޻෉


    ‣ ΑΓ࢖͍΍͍͢࢓૊ΈΛ໨ࢦͯ͠


    • ৽͍͠ΞϓϦ΁ͷԠ༻

    View Slide

  10. ΫοΫύουΞϓϦ
    • 50 Releases / year


    • 15 developers / release


    • 40 ~ 50 PR / release


    • 328,000 lines

    View Slide

  11. ୯ମىಈՄೳͳϞδϡʔϧߏ੒

    View Slide

  12. Cookpad TechConf 2019


    ʮΫοΫύου iOS ΞϓϦͷഁյͱ૝૾ɺͦͯ͠ະདྷʯ

    View Slide

  13. Cookpad TechConf 2019


    ʮΫοΫύου iOS ΞϓϦͷഁյͱ૝૾ɺͦͯ͠ະདྷʯ

    View Slide

  14. Core Module
    • ந৅ԽͷͨΊͷΠϯλʔϑΣΠε


    • ڞ௨ͷ UI ίϯϙʔωϯτ

    View Slide

  15. Cookpad TechConf 2019


    ʮΫοΫύου iOS ΞϓϦͷഁյͱ૝૾ɺͦͯ͠ະདྷʯ

    View Slide

  16. Application
    • Application Target


    • ϓϩδΣΫτ಺ͷ͢΂ͯͷ࣮૷Λ஌͍ͬͯΔ

    View Slide

  17. Cookpad TechConf 2019


    ʮΫοΫύου iOS ΞϓϦͷഁյͱ૝૾ɺͦͯ͠ະདྷʯ

    View Slide

  18. Feature Module
    • 1 ػೳʹؔ࿈͢Δෳ਺ը໘Λ 1 ͭͷϞδϡʔϧͱͯ͠·ͱΊΔ


    ‣ ങ͍෺ɺϨγϐৄࡉɺϨγϐ౤ߘɺͭ͘ΕΆɹͳͲ

    View Slide

  19. View Slide

  20. Feature Module
    • υϝΠϯ૚Λڞ༗Ͱ͖Δ୯ҐͰ෼ྨ͢Δ


    • Ϟδϡʔϧ୯ମͰ 1 ͭͷػೳʹඞཁͳ࣮૷͕׬݁͢Δ


    → Ϟδϡʔϧ୯ମͰϏϧυ͕Ͱ͖Δ 🎉

    View Slide

  21. View Slide

  22. View Slide

  23. ϛχΞϓϦͷ࢓૊Έ

    View Slide

  24. ϛχΞϓϦ࣮ݱͷ՝୊
    • ֎෦ϥΠϒϥϦ΁ͷґଘΛݮΒ͢


    • ଞͷ Feature Module ʹґଘ͠ͳ͍

    View Slide

  25. Transition
    Environment
    Network
    CookpadEnvironment StubbableEnvironment
    Logger
    Cookpad Sandbox Test
    ґଘͷ໰͍߹ΘͤΛ


    ड͚࣮ͯ૷Λฦ͢
    ґଘͷ஫ೖ

    View Slide

  26. Transition
    Environment
    Network
    CookpadEnvironment StubbableEnvironment
    Logger
    Cookpad Sandbox Test
    ֎෦ϥΠϒϥϦ΍ଞͷ


    ϞδϡʔϧΛ஌͍ͬͯΔ
    Ϟδϡʔϧ֎ͷ࣮૷ʹ


    ґଘ͠ͳ͍μϛʔ࣮૷

    View Slide

  27. Environment
    import Foundation
    import UIKit
    public protocol Environment
    {

    var client: ServiceClient { get
    }

    var serviceAccountProvider: ServiceAccountProvider { get
    }

    var activityLogger: ActivityLogger { get
    }

    var urlSchemeOpener: URLSchemeOpener { get
    }

    /// …
    func resolve(_ descriptor: Descriptor) ->
    Descriptor.Outpu
    t

    }

    View Slide

  28. Transition
    Environment
    Network
    CookpadEnvironment StubbableEnvironment
    Logger
    Cookpad Sandbox Test
    StubbableEnvironment
    ωοτϫʔΫ௨৴


    StubbableServiceClient
    ը໘ભҠ


    StubbableResolver

    View Slide

  29. Transition
    Environment
    Network
    CookpadEnvironment StubbableEnvironment
    Logger
    Cookpad Sandbox Test
    StubbableEnvironment
    ωοτϫʔΫ௨৴


    StubbableServiceClient

    View Slide

  30. class RecipeDetailsDataStoreTest: XCTestCase
    {

    let dataStore = RecipeDetailsDataStore(
    )

    private let recipeID: Int64 = 100
    func testSuccessFetchRecipeDetailsRecipes()
    {

    let expectation = self.expectation(description: "Could get recipe details
    recipe"
    )

    dataStore.request { (data, error
    )

    if let error = error
    {

    XCTFail(
    )

    } else
    {

    expectation.fulfill(
    )

    let recipe = try! JSONEncoder().decode(Recipe.self, from: data
    )

    XCTAssertEqual(recipe.name, "Ͱ͔͍ΠʔϒΠ"
    )

    XCTAssertEqual(recipe.title, "ϚϦτοπΥ"
    )

    }

    }

    waitUntilDefaultTimeout(for: expectation
    )

    }

    }

    View Slide

  31. class RecipeDetailsDataStoreTest: XCTestCase
    {

    let testingEnvironment = StubbableEnvironment(
    )

    lazy var dataStore = RecipeDetailsDataStore(environment: testingEnvironment
    )

    private let disposeBag = DisposeBag(
    )

    private let recipeID: Int64 = 100
    func testSuccessFetchRecipeDetailsRecipes()
    {

    let responseData = loadJSONData(named: "recipe_details_recipe"
    )

    testingEnvironment.registerClientResponse
    (

    responseData
    ,

    for: "/recipes/\(recipeID)"
    ,

    method: .get
    )

    let expectation = self.expectation(description: "Could get recipe details recipe"
    )

    dataStore.fetchRecipe(recipeID: recipeID
    )

    .subscribe(onSuccess: { response in
    expectation.fulfill(
    )

    XCTAssertEqual(response.author?.name, "Ͱ͔͍ΠʔϒΠ"
    )

    XCTAssertEqual(response.title, "ϚϦτοπΥ"
    )

    }
    )

    .disposed(by: disposeBag
    )

    waitUntilDefaultTimeout(for: expectation
    )

    }

    }

    View Slide

  32. public protocol GarageRequest
    {

    associatedtype Respons
    e

    var baseURL: URL? { get
    }

    var method: HTTPMethod { get
    }

    var path: String { get
    }

    var parameters: [String: Any] { get
    }

    var headerFields: [String: String] { get
    }

    func makeResponse(from data: Data, urlResponse: HTTPURLResponse) throws -> Respons
    e

    }

    public protocol ServiceClient
    {

    @discardableResul
    t

    func sendRequest
    (

    _ request: Request
    ,

    handler: @escaping (Result) -> Void) -> Cancellabl
    e

    }

    αʔόʔ΁ͷRequest


    ʹඞཁͳ΋ͷΛڞ௨Խ
    Request ϝιου

    View Slide

  33. struct ServiceClientStub
    {

    typealias Response = Swift.Result>

    var path: Strin
    g

    var method: HTTPMetho
    d

    var parameters: [String: Any
    ]

    var fields: [GarageFieldDescriptor]
    ?

    enum ParametersMatchingPattern
    {

    case exactMatc
    h

    case includin
    g

    }

    var parametersMatchingPattern: ParametersMatchingPatter
    n

    var result: Respons
    e

    var urlResponse: HTTPURLRespons
    e

    var parameterSet: Set
    {

    return makeParameterSet(from: parameters
    )

    }

    }

    RequestʹରԠ͢Δμϛʔ
    ͷResponseΛηοτͰ࣋ͭ
    Response
    Request

    View Slide

  34. // StubbableServiceClient
    private var responseStubs: [ServiceClientStub] = [
    ]

    func registerClientResponse
    (

    _ response: Response
    ,

    for path: String
    ,

    method: HTTPMethod
    ,

    statusCode: Int
    ,

    parameters: [String: Any] = [:]
    ,

    fields: [GarageFieldDescriptor]? = nil
    ,

    parametersMatchingPattern: ServiceClientStub.ParametersMatchingPattern
    ,

    httpHeaders: [String: String]? = nil
    )
    {

    let stub = ServiceClientStub
    (

    path: path
    ,

    method: method
    ,

    parameters: parameters
    ,

    fields: fields
    ,

    parametersMatchingPattern: parametersMatchingPattern
    ,

    result: .success(response.jsonData)
    ,

    urlResponse: makeURLResponse(path: path, statusCode: statusCode, httpHeaders:
    httpHeaders
    )

    )

    responseStubs.append(stub
    )

    }

    StubbableServiceClient
    ͷதͰStubΛอ࣋͢Δ
    ϝιουɾPath͝ͱʹҰҙʹ
    ͳΔΑ͏ʹϞοΫσʔλΛొ࿥

    View Slide

  35. let matchingStubs = responseStubs.filter { stub in
    if stub.path != request.path || stub.method != request.method
    {

    return false
    }

    if let stubFields = stub.fields, stubFields != request.fields
    {

    return false
    }

    switch stub.parametersMatchingPattern
    {

    case .exactMatch
    :

    return stub.parameterSet == request.parameterSe
    t

    case .including
    :

    return stub.parameterSet.isSubset(of: request.parameterSet
    )

    }

    }

    Pathɺύϥϝʔλɺ
    fi
    eld͕ొ࿥ࡁΈ


    ͷStubͱҰக͢Δ͔νΣοΫ

    View Slide

  36. public enum StubbingError: Error
    {

    case noMatchingStubs(AnyGarageRequest
    )

    case multipleMatchingStubs(AnyGarageRequest, [ServiceClientStub]
    )

    public var localizedDescription: String
    {

    switch self
    {

    case let .noMatchingStubs(request)
    :

    return "No matching stubs found for \(request)"
    case let .multipleMatchingStubs(request, stubs)
    :

    return "Multiple stubs matching \(request): \(stubs)"
    }

    }

    }

    // StubbableServiceClient
    let matchingStub: ServiceClientStu
    b

    switch matchingStubs.count
    {

    case 0
    :

    return stubbingErrorHandler(.noMatchingStubs(AnyGarageRequest(request))
    )

    case 1
    :

    matchingStub = matchingStubs[0
    ]

    default
    :

    return stubbingErrorHandler(.multipleMatchingStubs(AnyGarageRequest(request),
    matchingStubs)
    )

    }

    ద੾ͳελϒ͕ͳ͍ɺ·ͨ͸ॏෳͯ͠
    ଘࡏ͢Δ৔߹͸ErrorΛੜ੒͢Δ

    View Slide

  37. switch matchingStub.result
    {

    case let .success(data)
    :

    do
    {

    let response = try request.makeResponse(from: data ?? Data(), urlResponse:
    matchingStub.urlResponse
    )

    handler(.success(response)
    )

    } catch
    {

    switch error
    {

    case let clientTaskError as ClientTaskError
    :

    handler(.failure(clientTaskError)
    )

    case let responseError as ResponseError
    :

    handler(.failure(.rawNetworkingTaskError(.responseError(responseError)))
    )

    case let decodingError as DecodingError
    :

    handler(.failure(.rawNetworkingTaskError(.responseError(.serializationError(decodingError))))
    )

    default
    :

    handler(.failure(.rawNetworkingTaskError(.responseError(.unknownError(error))))
    )

    }

    }

    case let .failure(error)
    :

    handler(.failure(error)
    )

    }

    View Slide

  38. let environment = StubbableEnvironment(
    )

    let categoriesJSONData = fixtureLoader.loadJSONData(named: "categories"
    )

    environment.registerClientResponse
    (

    categoriesJSONData
    ,

    for: "/v1/top_categories"
    ,

    method: .get
    )

    let viewController = RecipeCategoryListViewBuilder.build(environment: environment
    )

    View Slide

  39. Transition
    Environment
    Network
    CookpadEnvironment StubbableEnvironment
    Logger
    Cookpad Sandbox Test
    StubbableEnvironment
    ը໘ભҠ


    StubbableResolver

    View Slide

  40. Resolver
    • ଞͷϞδϡʔϧͷ࣮૷ΛऔΓग़͢࢓૊Έ


    • Environment ܦ༝Ͱந৅Խ͞Εͨଞͷ FeatureModule ͷ࣮૷
    ΛऔΓग़͢

    View Slide

  41. Resolver for ϛχΞϓϦ
    • Resolver ͷ࢓૊ΈΛϛχΞϓϦͰ΋Ԡ༻


    • ଞϞδϡʔϧͷը໘͸શͯμϛʔͷը໘Ͱฦ͢

    View Slide

  42. ΫοΫύου։ൃऀϒϩά ίʔυੜ੒Λ༻͍ͨiOSΞϓϦϚϧνϞδϡʔϧԽͷͨΊͷґଘղܾ


    https://techlife.cookpad.com/entry/2021/06/16/110000

    View Slide

  43. View Slide

  44. ͜͜·Ͱͷ·ͱΊ
    • Ϟδϡʔϧ୯ҐͰҰͭͷػೳ͕׬݁͢ΔΑ͏ʹ෼཭͢Δ


    • Ϟδϡʔϧಉ࢜͸૬ޓґଘ͠ͳ͍


    • ଞϞδϡʔϧʹ͋ΔͳͲ௚઀ࢀরͰ͖ͳ͍෭࡞༻͸μϛʔ࣮૷
    Λ༻ҙ͢Δ

    View Slide

  45. νʔϜͰར༻ͯ͠΋Β͏޻෉

    View Slide

  46. ϛχΞϓϦಋೖॳظͷ՝୊
    • ϛχΞϓϦ༻ͷίʔυͷهड़͕ͱʹ͔͘໘౗


    • ಉ͡ηοτΞοϓΛԿճ΋΍Γͨ͘ͳ͍

    View Slide

  47. ίʔυͷڞ༗ɾڞ௨Խ

    View Slide

  48. public struct SandboxScene
    {

    public init(sceneName: String, initializer: @escaping (SandboxInitializer) ->
    UIViewController)
    {

    self.sceneName = sceneNam
    e

    self.initializer = initialize
    r

    }

    var sceneName: Strin
    g

    var initializer: (SandboxInitializer) -> UIViewControlle
    r

    }

    // AppDelegate.swift
    let rootViewController = SandboxSceneSelectTableViewController
    (

    scenes:
    [

    .recipeCategoryList
    ,

    .subCategories
    ,

    ]

    )

    Sandboxͷը໘


    Λߏ੒͢Δ΋ͷ
    SandboxScene


    ͷ഑ྻΛڞ༗

    View Slide

  49. View Slide

  50. ίʔυͷڞ௨Խ
    • ϛχΞϓϦ༻ͷμϛʔ෭࡞༻͕ͲΜͲΜ૿͑Δ


    • ىಈ࣌ʹඞཁͳίʔυ͕૿͑Δ

    View Slide

  51. CookpadCore
    CookpadComponent
    SandboxCore
    Feature B
    Sandbox
    Feature A
    Sandbox
    Feature C
    Sandbox
    Sandbox༻ͷڞ௨࣮૷
    ຊମͱͷڞ௨࣮૷

    View Slide

  52. ίʔυͷࣗಈੜ੒

    View Slide

  53. ίʔυͷࣗಈੜ੒
    • AppDelegate ΍֤ Scene ͷॳظઃఆͳͲɺϘΠϥʔϓϨʔτ
    ίʔυͷهड़͕໘౗


    → ίʔυ͸πʔϧΛ࢖ͬͯࣗಈੜ੒͢Δ

    View Slide

  54. Genesis
    • https://github.com/yonaskolb/Genesis


    • Stencil ͱ͍͏ςϯϓϨʔτΤϯδϯΛར༻

    View Slide

  55. options
    :

    - name: sceneName
    question: Sandbox scene name?
    description: new Sandbox scene name to generate. (e.g. recipeDetails).
    type: string
    required: true
    - name: moduleName
    question: Destination target?
    description: module name to generate new sandbox scene for. (e.g. RecipeDetails)
    type: string
    required: true
    files
    :

    - template: Sandbox/SandboxScene.stencil
    path: "Sandbox/{{ moduleName }}Sandbox/{{ sceneName }}SandboxScene.swift"
    - template: Sandbox/AppDelegate.swift.stencil
    path: "Sandbox/{{ moduleName }}Sandbox/{{ moduleName }}AppDelegate.swift"

    View Slide

  56. View Slide

  57. @testable import {{ moduleName }
    }

    import UIKit
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate
    {

    private let environment = StubbableEnvironment(
    )

    var window: UIWindow
    ?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
    {

    window = UIWindow(frame: UIScreen.main.bounds
    )

    // Inject Scenes to RootTableViewController
    let rootViewController = {{ sceneName }}ViewBuilder.build(environment: environment
    )

    window?.rootViewController = rootViewControlle
    r

    window?.makeKeyAndVisible(
    )

    return true
    }

    }

    https://github.com/stencilproject/Stencil

    View Slide

  58. @testable import RecipeDetails
    import UIKit
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate
    {

    private let environment = StubbableEnvironment(
    )

    var window: UIWindow
    ?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
    {

    window = UIWindow(frame: UIScreen.main.bounds
    )

    // Inject Scenes to RootTableViewController
    let rootViewController = RecipeDetailsViewBuilder.build(environment: environment
    )

    window?.rootViewController = rootViewControlle
    r

    window?.makeKeyAndVisible(
    )

    return true
    }

    }

    View Slide

  59. Sandbox ࠲ஊձͷ։࠵

    View Slide

  60. ΑΓ࢖͍΍͍͢࢓૊ΈΛ໨ࢦͯ͠

    View Slide

  61. ࠷ޙʹىಈͨ͠ը໘ΛهԱ
    • ։ൃத͸ಉ͡ը໘Λ܁Γฦ֬͠ೝ͍ͨ͠৔߹͕ଟ͍


    → ࠷ޙʹ։͍ͨ Scene ΛهԱͯ͠ɺىಈ࣌ʹͦΕΛ։͘Α͏ʹ

    View Slide

  62. View Slide

  63. ωοτϫʔΫͷ஗Ԇͷઃఆ
    • ελϒͨ͠ϨεϙϯεΛฦ͢෦෼ͰਓҝతʹϦΫΤετॲཧΛ
    ஗Ԇͤ͞ΔΑ͏ʹ
    // StubbableServiceClient.swift
    open func sendRequest
    (

    _ request: Request
    ,

    handler: @escaping (Result) -> Void) -> Cancellable
    where Request: GarageRequest
    {

    let workItem = DispatchWorkItem
    {

    self.handleSendRequest(request, handler: handler
    )

    }

    DispatchQueue.main.asyncAfter(deadline: .now() + requestDelayDuration, execute: workItem
    )

    return workIte
    m

    }

    View Slide

  64. ͔͜͜Βཧ૝࿦

    View Slide

  65. ࣮ϦΫΤετͷ࣮ݱ
    • ୯ػೳͰͷ API ͷૄ௨֬ೝ


    • σʔλͷελϒ͕ཁΒͳ͍ͷͰศར

    View Slide

  66. ελϒͷ؆қԽ
    • Ϩεϙϯεͷ JSON Λ౎౓खಈͰ༻ҙͯͯ͠େม


    • ϞοΫσʔλੜ੒πʔϧͷར༻Λݕ౼

    View Slide

  67. Advanced

    View Slide

  68. ৽͍͠ΞϓϦ΁ͷԠ༻

    View Slide

  69. Cookpad Mart
    • 2018 ೥ʹ։࢝ͨ͠৽͍͠ϓϩμΫτ


    • ը໘΍ػೳ͕૿͑ෳࡶʹͳ͖ͬͯͨ


    • ؔΘͬͨ։ൃऀ΋૿͖͑ͯͨʢ㲈 ίʔυ͕൥ࡶʹͳ͖ͬͯͨʣ

    View Slide

  70. Cookpad Mart ͷ՝୊
    • ֤ঢ়ଶͷ࠶ݱ৚͕݅ෳࡶ


    • ը໘ભҠ͕໘౗

    View Slide

  71. Ϟδϡʔϧ෼཭·Ͱ͸Ͱ͖ͯͳ͍͚Ͳ


    ಈ࡞֬ೝΛগ͠Ͱ΋ָʹ͍ͨ͠ʂ

    View Slide

  72. ୯ը໘ىಈͷΈͷ༌ೖ

    View Slide

  73. ୯ը໘ىಈ
    • Scheme ͷ؀ڥม਺Λར༻


    • XcodeGenΛ࢖͍ͬͯΕ͹༰қʹઃఆՄೳ

    View Slide

  74. schemes
    :

    CookpadMartECSandbox
    :

    build
    :

    config: Debug
    targets
    :

    CookpadMartEC: all
    run
    :

    config: Debug
    environmentVariables
    :

    RUNNING_SANDBOX: 1
    profile
    :

    config: Debug

    View Slide

  75. View Slide

  76. func application
    (

    _ application: UIApplication
    ,

    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]
    ?

    ) -> Bool
    {

    if isRunningSandbox
    {

    window = UIWindow(frame: UIScreen.main.bounds
    )

    let rootViewController = SandboxSceneSelectTableViewController
    {

    Scene
    (

    name: "Coupon"
    ,

    CouponListViewController.create(dependency: .init(launchMode: .setting, shouldCloseButtonHidden: false)
    )

    )
    {

    Mock(path: "/v2/coupons", statusCode: 200, method: .get)
    {

    JSONData(fromBundle: "coupons"
    )

    }

    }

    Scene
    (

    name: "Coupon (Error)"
    ,

    CouponListViewController.create(dependency: .init(launchMode: .setting, shouldCloseButtonHidden: false)
    )

    )
    {

    Mock(path: "/v2/coupons", statusCode: 403, method: .get)
    {

    JSONData("\"error\": \"message\""
    )

    }

    }

    }

    let navigationController = UINavigationController(rootViewController: rootViewController
    )

    window?.rootViewController = navigationControlle
    r

    window?.makeKeyAndVisible(
    )

    return true
    }

    /// ....
    return true
    }

    View Slide

  77. ϦΫΤετ͸ελϒ͢Δ
    let stubbableServiceClient = StubbableServiceClient(
    )

    if let jsonData = mock.json.data
    {

    stubbableServiceClient.registerClientResponse
    (

    jsonData
    ,

    for: mock.path
    ,

    method: mock.method
    ,

    statusCode: mock.statusCod
    e

    )

    }

    ServiceClientProvider.injectionContainer.serviceClient = stubbableServiceClien
    t

    View Slide

  78. ୯ը໘ىಈ
    • ఆٛͨ͠ Scheme Λબ୒ͯ͠Ϗϧυ͢Δ

    View Slide

  79. View Slide

  80. ୯ը໘ىಈ
    • ୯ը໘ىಈͷ࢓૊Έ͕Θ͔Δ SandboxKit


    • https://github.com/aomathwift/SandboxKit

    View Slide

  81. ·ͱΊ

    View Slide

  82. ·ͱΊ
    • ϛχΞϓϦ͸։ൃޮ཰Λ্͛ΔҰखஈ


    ‣ ػೳ୯ҐͰΘ͚Δ


    ‣ ґଘΛݮΒ͢


    ‣ ηοτΞοϓΛ؆୯ʹ͢Δ


    ‣ ୯ը໘ػೳ͚ͩͰ΋݁ߏศར

    View Slide

  83. એ఻

    View Slide

  84. એ఻1
    • େن໛ͳΞϓϦͷϚϧνϞδϡʔϧߏ੒ͷ࣮ફ

    View Slide

  85. એ఻2
    • After Party iOSDC Japan 2021


    • https://cookpad.connpass.com/event/222056

    View Slide

  86. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠

    View Slide