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

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 full-size slide

  2. SINGAPORE NOW
    @CARROT_App

    View full-size slide

  3. MADISON NOW
    @CARROT_App

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. @DesignatedNerd

    View full-size slide

  7. SPOILER:
    THERE IS A BETTER WAY
    @DesignatedNerd

    View full-size slide

  8. SCRIPTING WITH

    SWIFT
    @DesignatedNerd

    View full-size slide

  9. @DesignatedNerd

    View full-size slide

  10. !
    HOW DOES APOLLO WORK?
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  12. GraphQL
    @DesignatedNerd

    View full-size slide

  13. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View full-size slide

  14. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  16. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  18. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  20. !
    @DesignatedNerd

    View full-size slide

  21. !
    @DesignatedNerd

    View full-size slide

  22. SCHEMA + QUERIES = CODE
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  24. JAVASCRIPT + NODE
    @DesignatedNerd

    View full-size slide

  25. TYPESCRIPT + NODE
    @DesignatedNerd

    View full-size slide

  26. via Webcomic Name and Alex Norris

    View full-size slide

  27. npm =
    @DesignatedNerd

    View full-size slide

  28. HOW DO WE MAKE THIS
    BETTER?
    @DesignatedNerd

    View full-size slide

  29. !!!!
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  31. BUNDLING EVERYTHING
    @DesignatedNerd

    View full-size slide

  32. BUNDLING EVERYTHING
    A SMALLER
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. @DesignatedNerd

    View full-size slide

  36. I HATE BASH
    @DesignatedNerd

    View full-size slide

  37. I HATE SUCK AT BASH
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  41. !
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  43. LET'S DO THIS!
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  45. IDENTIFY WHAT YOUR SCRIPT DOES
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  47. run_bundled_codegen.sh
    @DesignatedNerd

    View full-size slide

  48. WHAT APOLLO'S SCRIPT DOES
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  53. 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 full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

  56. 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 full-size 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?
    YES
    Unzip if needed
    @DesignatedNerd

    View full-size slide

  58. RUN THE CLI WITH THE PASSED IN INFORMATION

    GENERATE CODE
    @DesignatedNerd

    View full-size slide

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

    ApolloCodegenOptions
    @DesignatedNerd

    View full-size slide

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

    FileManager
    @DesignatedNerd

    View full-size slide

  61. curl

    URLSession
    @DesignatedNerd

    View full-size slide

  62. /usr/bin/shasum

    CommonCrypto
    @DesignatedNerd

    View full-size slide

  63. SEPARATION OF CONCERNS
    @DesignatedNerd

    View full-size slide

  64. !
    DEPENDENCY INJECTION
    @DesignatedNerd

    View full-size slide

  65. CODE COVERAGE
    @DesignatedNerd

    View full-size slide

  66. @DesignatedNerd

    View full-size slide

  67. @DesignatedNerd

    View full-size slide

  68. @DesignatedNerd

    View full-size slide

  69. !
    BEST PRACTICES, DUH
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  75. chmod +x main.swift
    @DesignatedNerd

    View full-size slide

  76. @DesignatedNerd

    View full-size slide

  77. @DesignatedNerd

    View full-size slide

  78. @DesignatedNerd

    View full-size slide

  79. swift run
    @DesignatedNerd

    View full-size slide

  80. DON'T DO THIS
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

  84. 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 full-size slide

  85. 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 full-size 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 full-size 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 full-size slide

  88. @DesignatedNerd

    View full-size slide

  89. $SRCROOT
    @DesignatedNerd

    View full-size slide

  90. @DesignatedNerd

    View full-size slide

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

    View full-size slide

  92. 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 full-size slide

  93. 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 full-size slide

  94. 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 full-size slide

  95. 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 full-size slide

  96. 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 full-size slide

  97. 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 full-size 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 full-size slide

  99. IN YOUR APPLICATION PROJECT
    @DesignatedNerd

    View full-size slide

  100. 3.
    !
    PROFIT!
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  102. A CAVEAT
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  104. ! "
    @DesignatedNerd

    View full-size slide

  105. WRITE THE DOCS
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

  107. OBLIGATORY SUMMARY SLIDE
    @DesignatedNerd

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  111. 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 full-size slide

  112. 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 full-size slide

  113. 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 full-size slide

  114. 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 full-size 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
    > 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 full-size slide

  116. !
    THANK YOU!
    @DesignatedNerd

    View full-size slide

  117. 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 full-size slide