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

Scripting in Swift For a Testable Build - iOS Conf SG, Singapore, January 2020

Scripting in Swift For a Testable Build - iOS Conf SG, Singapore, January 2020

Abstract:

Underlying many large iOS projects is a tangled nest of bash scripts that developers are often afraid to touch for fear of breaking something, and which is littered with print statements from generations of developers trying to figure out how on earth it works. How can we move away from this mess and to something more sustainable? Ellen will discuss moving a codebase like this to a command line tool that can be called from a Swift script, and which can be tested and breakpointed, and just might save a tiny bit of your sanity.

Ellen Shapiro
PRO

January 17, 2020
Tweet

More Decks by Ellen Shapiro

Other Decks in Technology

Transcript

  1. SCRIPTING IN SWIFT FOR A
    TESTABLE BUILD
    IOS CONF SG | SINGAPORE | JANUARY 2020
    ELLEN SHAPIRO | @DESIGNATEDNERD | APOLLOGRAPHQL.COM

    View Slide

  2. View Slide

  3. SINGAPORE NOW
    @CARROT_App

    View Slide

  4. MADISON NOW
    @CARROT_App

    View Slide

  5. YOUR IOS PROJECT HAS
    A NEST OF BASH SCRIPTS
    @DesignatedNerd

    View Slide

  6. YOUR IOS PROJECT HAS
    A NEST OF BASH SCRIPTS
    (IT'S OKAY, MY PROJECTS DO TOO)
    @DesignatedNerd

    View Slide

  7. @DesignatedNerd

    View Slide

  8. View Slide

  9. SPOILER:
    THERE IS A BETTER WAY
    @DesignatedNerd

    View Slide

  10. SCRIPTING WITH

    SWIFT
    @DesignatedNerd

    View Slide

  11. @DesignatedNerd

    View Slide

  12. !
    HOW DOES APOLLO WORK?
    @DesignatedNerd

    View Slide

  13. !
    HOW DOES APOLLO WORK?
    (THE SHORT VERSION)
    @DesignatedNerd

    View Slide

  14. GraphQL
    @DesignatedNerd

    View Slide

  15. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View Slide

  16. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View Slide

  17. SCHEMA + QUERIES = CODE
    WHAT IS POSSIBLE?
    @DesignatedNerd

    View Slide

  18. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View Slide

  19. SCHEMA + QUERIES = CODE
    WHAT AM I ASKING FOR?
    @DesignatedNerd

    View Slide

  20. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View Slide

  21. SCHEMA + QUERIES = CODE
    IS WHAT I'M ASKING FOR POSSIBLE?
    @DesignatedNerd

    View Slide

  22. !
    @DesignatedNerd

    View Slide

  23. View Slide

  24. !
    @DesignatedNerd

    View Slide

  25. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View Slide

  26. SCHEMA + QUERIES = CODE
    HAVE SOME TYPE-SAFE CODE!
    @DesignatedNerd

    View Slide

  27. JAVASCRIPT + NODE
    @DesignatedNerd

    View Slide

  28. TYPESCRIPT + NODE
    @DesignatedNerd

    View Slide

  29. via Webcomic Name and Alex Norris

    View Slide

  30. npm =
    @DesignatedNerd

    View Slide

  31. View Slide

  32. HOW DO WE MAKE THIS
    BETTER?
    @DesignatedNerd

    View Slide

  33. !!!!
    @DesignatedNerd

    View Slide

  34. HOW DO WE MAKE THIS
    LESS TERRIBLE
    AS QUICKLY AS POSSIBLE?
    @DesignatedNerd

    View Slide

  35. BUNDLING EVERYTHING
    @DesignatedNerd

    View Slide

  36. BUNDLING EVERYTHING
    A SMALLER
    @DesignatedNerd

    View Slide

  37. BEFORE
    LOTS OF BASH YOU HAD TO WRITE
    @DesignatedNerd

    View Slide

  38. AFTER
    LOTS OF BASH I HAD TO WRITE
    @DesignatedNerd

    View Slide

  39. @DesignatedNerd

    View Slide

  40. I HATE BASH
    @DesignatedNerd

    View Slide

  41. I HATE SUCK AT BASH
    @DesignatedNerd

    View Slide

  42. TESTING AND DEBUGGING BASH:
    ¯\_(ϑ)_/¯
    @DesignatedNerd

    View Slide

  43. echo "Variable: ${VARIABLE}"
    @DesignatedNerd

    View Slide

  44. FIGURING OUT WHAT WENT WRONG
    IN BASH: ¯\_(ϑ)_/¯
    @DesignatedNerd

    View Slide

  45. !
    @DesignatedNerd

    View Slide

  46. DON'T IT ALWAYS SEEM TO GO
    THAT YOU DON'T KNOW
    WHAT YOU GOT
    'TIL IT'S GONE
    - JONI MITCHELL
    @DesignatedNerd

    View Slide

  47. LET'S DO THIS!
    @DesignatedNerd

    View Slide

  48. 1. BUILD A SWIFT FRAMEWORK
    THAT DOES WHAT YOUR SCRIPT CURRENTLY DOES
    @DesignatedNerd

    View Slide

  49. IDENTIFY WHAT YOUR SCRIPT DOES
    @DesignatedNerd

    View Slide

  50. IDENTIFY WHAT YOUR SCRIPT DOES
    (THIS SOUNDS MUCH EASIER THAN IT IS)
    @DesignatedNerd

    View Slide

  51. run_bundled_codegen.sh
    @DesignatedNerd

    View Slide

  52. WHAT APOLLO'S SCRIPT DOES
    @DesignatedNerd

    View Slide

  53. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    @DesignatedNerd

    View Slide

  54. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    NO
    @DesignatedNerd

    View Slide

  55. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    NO
    Download and unzip current version of CLI.
    @DesignatedNerd

    View Slide

  56. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    YES
    @DesignatedNerd

    View Slide

  57. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    YES
    Does the zipped version of the CLI have the correct hash
    for this version of the iOS SDK?
    @DesignatedNerd

    View Slide

  58. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    YES
    Does the zipped version of the CLI have the correct hash
    for this version of the iOS SDK?
    NO
    @DesignatedNerd

    View Slide

  59. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    YES
    Does the zipped version of the CLI have the correct hash
    for this version of the iOS SDK?
    NO
    Download and unzip current version of CLI.
    @DesignatedNerd

    View Slide

  60. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    YES
    Does the zipped version of the CLI have the correct hash
    for this version of the iOS SDK?
    YES
    @DesignatedNerd

    View Slide

  61. WHAT APOLLO'S SCRIPT DOES
    Is there a zipped version of the CLI?
    YES
    Does the zipped version of the CLI have the correct hash
    for this version of the iOS SDK?
    YES
    Unzip if needed
    @DesignatedNerd

    View Slide

  62. RUN THE CLI WITH THE PASSED IN INFORMATION

    GENERATE CODE
    @DesignatedNerd

    View Slide

  63. codegen:generate --target=swift
    --localSchemaFile="schema.json"
    --includes=./**/*.graphql
    --operationIdsPath=operationIdsPath.json
    --suppressSwiftMultilineStringLiterals

    ApolloCodegenOptions
    @DesignatedNerd

    View Slide

  64. if [ -f "${ZIP_FILE}" ]

    FileManager
    @DesignatedNerd

    View Slide

  65. curl

    URLSession
    @DesignatedNerd

    View Slide

  66. /usr/bin/shasum

    CommonCrypto
    @DesignatedNerd

    View Slide

  67. SEPARATION OF CONCERNS
    @DesignatedNerd

    View Slide

  68. !
    DEPENDENCY INJECTION
    @DesignatedNerd

    View Slide

  69. CODE COVERAGE
    @DesignatedNerd

    View Slide

  70. @DesignatedNerd

    View Slide

  71. @DesignatedNerd

    View Slide

  72. @DesignatedNerd

    View Slide

  73. !
    BEST PRACTICES, DUH
    @DesignatedNerd

    View Slide

  74. YOUR SCRIPTS DESERVE
    TO BE FIRST-CLASS CITIZENS
    @DesignatedNerd

    View Slide

  75. IF YOU CAN'T RELEASE YOUR APP WITHOUT IT
    TEST IT
    @DesignatedNerd

    View Slide

  76. IF YOU CAN'T RELEASE YOUR APP WITHOUT IT
    TEST IT
    (AND IT'S A LOT EASIER TO TEST IT IN SWIFT)
    @DesignatedNerd

    View Slide

  77. 2. USE THE LIBRARY IN A
    SPM EXECUTABLE
    @DesignatedNerd

    View Slide

  78. #! /usr/bin/env xcrun swift -F ../build/Debug
    @DesignatedNerd

    View Slide

  79. chmod +x main.swift
    @DesignatedNerd

    View Slide

  80. @DesignatedNerd

    View Slide

  81. @DesignatedNerd

    View Slide

  82. @DesignatedNerd

    View Slide

  83. swift run
    @DesignatedNerd

    View Slide

  84. DON'T DO THIS
    @DesignatedNerd

    View Slide

  85. CREATE IT WITH SWIFT PACKAGE MANAGER
    $ mkdir Codegen
    $ cd Codegen
    $ swift package init --type executable
    @DesignatedNerd

    View Slide

  86. Package.swift
    // swift-tools-version:5.1
    import PackageDescription
    let package = Package(
    name: "Codegen",
    products: [
    .executable(name: "Codegen", targets: ["Codegen"]),
    ],
    dependencies: [
    .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen"))
    ],
    targets: [
    .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]),
    .testTarget(name: "CodegenTests", dependencies: ["Codegen"]),
    ]
    )
    @DesignatedNerd

    View Slide

  87. Package.swift
    // swift-tools-version:5.1
    import PackageDescription
    let package = Package(
    name: "Codegen",
    products: [
    .executable(name: "Codegen", targets: ["Codegen"]),
    ],
    dependencies: [
    .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen"))
    ],
    targets: [
    .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]),
    .testTarget(name: "CodegenTests", dependencies: ["Codegen"]),
    ]
    )
    @DesignatedNerd

    View Slide

  88. Package.swift
    // swift-tools-version:5.1
    import PackageDescription
    let package = Package(
    name: "Codegen",
    products: [
    .executable(name: "Codegen", targets: ["Codegen"]),
    ],
    dependencies: [
    .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen"))
    ],
    targets: [
    .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]),
    .testTarget(name: "CodegenTests", dependencies: ["Codegen"]),
    ]
    )
    @DesignatedNerd

    View Slide

  89. Package.swift
    // swift-tools-version:5.1
    import PackageDescription
    let package = Package(
    name: "Codegen",
    products: [
    .executable(name: "Codegen", targets: ["Codegen"]),
    ],
    dependencies: [
    .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen"))
    ],
    targets: [
    .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]),
    .testTarget(name: "CodegenTests", dependencies: ["Codegen"]),
    ]
    )
    @DesignatedNerd

    View Slide

  90. Package.swift
    // swift-tools-version:5.1
    import PackageDescription
    let package = Package(
    name: "Codegen",
    products: [
    .executable(name: "Codegen", targets: ["Codegen"]),
    ],
    dependencies: [
    .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen"))
    ],
    targets: [
    .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]),
    .testTarget(name: "CodegenTests", dependencies: ["Codegen"]),
    ]
    )
    @DesignatedNerd

    View Slide

  91. Package.swift
    // swift-tools-version:5.1
    import PackageDescription
    let package = Package(
    name: "Codegen",
    products: [
    .executable(name: "Codegen", targets: ["Codegen"]),
    ],
    dependencies: [
    .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen"))
    ],
    targets: [
    .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]),
    .testTarget(name: "CodegenTests", dependencies: ["Codegen"]),
    ]
    )
    @DesignatedNerd

    View Slide

  92. @DesignatedNerd

    View Slide

  93. $SRCROOT
    @DesignatedNerd

    View Slide

  94. @DesignatedNerd

    View Slide

  95. main.swift
    print("Hello, world!")
    @DesignatedNerd

    View Slide

  96. main.swift
    import Foundation
    enum MyCodegenError: Error {
    case sourceRootNotProvided
    case sourceRootNotADirectory
    case targetDoesntExist
    }
    guard let sourceRootPath = ProcessInfo.processInfo.environment["SRCROOT"] else {
    throw MyCodegenError.sourceRootNotProvided
    }
    @DesignatedNerd

    View Slide

  97. main.swift
    import Foundation
    import ApolloCodegenLib
    enum MyCodegenError: Error {
    case sourceRootNotProvided
    case sourceRootNotADirectory
    case targetDoesntExist
    }
    guard let sourceRootPath = ProcessInfo.processInfo.environment["SRCROOT"] else {
    throw MyCodegenError.sourceRootNotProvided
    }
    guard FileManager.default.apollo_folderExists(at: sourceRootPath) else {
    throw MyCodegenError.sourceRootNotADirectory
    }
    @DesignatedNerd

    View Slide

  98. main.swift (pt. 2)
    let applicationTargetFolderURL = sourceRootURL
    .appendingPathComponent("SwiftScriptTestSPM")
    guard FileManager.default.apollo_folderExists(at: applicationTarget) else {
    throw MyCodegenError.targetDoesntExist
    }
    let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts")
    let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL)
    do {
    try ApolloCodegen.run(from: applicationTargetFolderURL,
    with: scriptsFolderURL,
    options: options)
    } catch {
    CodegenLogger.log("ERROR: \(error.localizedDescription)")
    exit(1)
    }
    @DesignatedNerd

    View Slide

  99. main.swift (pt. 2)
    let applicationTargetFolderURL = sourceRootURL
    .appendingPathComponent("SwiftScriptTestSPM")
    guard FileManager.default.apollo_folderExists(at: applicationTarget) else {
    throw MyCodegenError.targetDoesntExist
    }
    let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts")
    let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL)
    do {
    try ApolloCodegen.run(from: applicationTargetFolderURL,
    with: scriptsFolderURL,
    options: options)
    } catch {
    CodegenLogger.log("ERROR: \(error.localizedDescription)")
    exit(1)
    }
    @DesignatedNerd

    View Slide

  100. main.swift (pt. 2)
    let applicationTargetFolderURL = sourceRootURL
    .appendingPathComponent("SwiftScriptTestSPM")
    guard FileManager.default.apollo_folderExists(at: applicationTarget) else {
    throw MyCodegenError.targetDoesntExist
    }
    let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts")
    let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL)
    do {
    try ApolloCodegen.run(from: applicationTargetFolderURL,
    with: scriptsFolderURL,
    options: options)
    } catch {
    CodegenLogger.log("ERROR: \(error.localizedDescription)")
    exit(1)
    }
    @DesignatedNerd

    View Slide

  101. main.swift (pt. 2)
    let applicationTargetFolderURL = sourceRootURL
    .appendingPathComponent("SwiftScriptTestSPM")
    guard FileManager.default.apollo_folderExists(at: applicationTarget) else {
    throw MyCodegenError.targetDoesntExist
    }
    let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts")
    let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL)
    do {
    try ApolloCodegen.run(from: applicationTargetFolderURL,
    with: scriptsFolderURL,
    options: options)
    } catch {
    CodegenLogger.log("ERROR: \(error.localizedDescription)")
    exit(1)
    }
    @DesignatedNerd

    View Slide

  102. main.swift (pt. 2)
    let applicationTargetFolderURL = sourceRootURL
    .appendingPathComponent("SwiftScriptTestSPM")
    guard FileManager.default.apollo_folderExists(at: applicationTarget) else {
    throw MyCodegenError.targetDoesntExist
    }
    let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts")
    let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL)
    do {
    try ApolloCodegen.run(from: applicationTargetFolderURL,
    with: scriptsFolderURL,
    options: options)
    } catch {
    CodegenLogger.log("ERROR: \(error.localizedDescription)")
    exit(1)
    }
    @DesignatedNerd

    View Slide

  103. IN YOUR APPLICATION PROJECT
    @DesignatedNerd

    View Slide

  104. 3.
    !
    PROFIT!
    @DesignatedNerd

    View Slide

  105. 3.
    !
    PROFIT!*
    * - BY WASTING LESS OF YOUR TIME FAFFING AROUND WITH BASH
    @DesignatedNerd

    View Slide

  106. A CAVEAT
    @DesignatedNerd

    View Slide

  107. DEVELOPERS ARE NOT
    USED TO SCRIPTING IN SWIFT
    @DesignatedNerd

    View Slide

  108. ! "
    @DesignatedNerd

    View Slide

  109. WRITE THE DOCS
    @DesignatedNerd

    View Slide

  110. COMING SOON
    TO AN APOLLO IOS SDK NEAR YOU!
    @DesignatedNerd

    View Slide

  111. OBLIGATORY SUMMARY SLIDE
    @DesignatedNerd

    View Slide

  112. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    @DesignatedNerd

    View Slide

  113. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    > Know what broke when stuff breaks
    @DesignatedNerd

    View Slide

  114. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    > Know what broke when (not if) stuff breaks
    @DesignatedNerd

    View Slide

  115. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    > Know what broke when (not if) stuff breaks
    > Dissect your old scripts, rebuild in a Swift Framework
    @DesignatedNerd

    View Slide

  116. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    > Know what broke when (not if) stuff breaks
    > Dissect your old scripts, rebuild in a Swift Framework
    > Test everything you can't release your app without
    @DesignatedNerd

    View Slide

  117. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    > Know what broke when (not if) stuff breaks
    > Dissect your old scripts, rebuild in a Swift Framework
    > Test everything you can't release your app without
    > Use Swift Package Manager to build an executable
    @DesignatedNerd

    View Slide

  118. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    > Know what broke when (not if) stuff breaks
    > Dissect your old scripts, rebuild in a Swift Framework
    > Test everything you can't release your app without
    > Use Swift Package Manager to build an executable
    > Run that from a build script run phase in your main app
    @DesignatedNerd

    View Slide

  119. OBLIGATORY SUMMARY SLIDE
    > Use swift to write testable script code
    > Know what broke when (not if) stuff breaks
    > Dissect your old scripts, rebuild in a Swift Framework
    > Test everything you can't release your app without
    > Use Swift Package Manager to build an executable
    > Run that from a build script run phase in your main app
    > Document the crap out of everything
    @DesignatedNerd

    View Slide

  120. !
    THANK YOU!
    @DesignatedNerd

    View Slide

  121. LINKS!
    ADOPTING SWIFT PACKAGES IN XCODE
    https://developer.apple.com/videos/play/wwdc2019/408/
    CREATING SWIFT PACKAGES
    https://developer.apple.com/videos/play/wwdc2019/410/
    @DesignatedNerd

    View Slide