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

[Arc Latam Job Fair] Exploring the power of Swi...

Pol Piella Abadia
November 17, 2022
950

[Arc Latam Job Fair] Exploring the power of Swift beyond app development

Pol Piella Abadia

November 17, 2022
Tweet

More Decks by Pol Piella Abadia

Transcript

  1. Pol Piella Abadia - 📍Arc Latam Job Fair 🌍 -

    17/11/22 Exploring the power of Swift beyond App development @polpielladev
  2. 🧑💻 Senior iOS Engineer @ BBC iPlayer 📍 From Barcelona

    🇬🇧 Based in Manchester ❤ Swift and developer tooling 👕 Random fact: I collect football shirts! Hi, I’m Pol 👋
  3. 🔨 I do as much as possible with Swift 📲

    Swift way beyond app development 🔧 A lot of interesting Swift tools 🤞 Encourage you to use Swift more 📜 From script to full-stack 📦 A Swift package monorepo 🧑💻 Code examples for everyone! A bit of context 🔊
  4. ⛑ Type/Compiler safety 🦶 Low memory footprint 🤝 Great development

    experience 🧐 Familiarity 🌟 Mature and amazing community Why Swift though? https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf
  5. 🔧 A tool to get an estimated reading time for

    a markdown fi le 📖 Inspired by Medium’s reading time feature - number of words / wpm. ✍ Written entirely in Swift 📦 Distributed as a Swift Package with products such as a library, executables, APIs and playgrounds. 🏃 I’ll take you through how to get it running everywhere with just Swift! Hi, Reading Time 👋
  6. Where it all began… 🔨 #!/usr/bin/env swift import Foundation //

    First argument is the path to the script if CommandLine.arguments.count != 2 { let argumentCount = CommandLine.arguments.count - 1 print("Expected 1 argument but got: \(argumentCount) instead...") exit(1) }
  7. Where it all began… 🔨 #!/usr/bin/env swift import Foundation //

    First argument is the path to the script if CommandLine.arguments.count != 2 { let argumentCount = CommandLine.arguments.count - 1 print("Expected 1 argument but got: \(argumentCount) instead...") exit(1) } let pathToFile = CommandLine.arguments[1]
  8. Where it all began… 🔨 #!/usr/bin/env swift import Foundation //

    First argument is the path to the script if CommandLine.arguments.count != 2 { let argumentCount = CommandLine.arguments.count - 1 print("Expected 1 argument but got: \(argumentCount) instead...") exit(1) } let pathToFile = CommandLine.arguments[1] guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(pathToFile)"), let contents = String(data: data, encoding: .utf8) else { print("🛑 Could not get contents for file...") exit(1) }
  9. Where it all began… 🔨 #!/usr/bin/env swift import Foundation //

    First argument is the path to the script if CommandLine.arguments.count != 2 { let argumentCount = CommandLine.arguments.count - 1 print("Expected 1 argument but got: \(argumentCount) instead...") exit(1) } let pathToFile = CommandLine.arguments[1] guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(pathToFile)"), let contents = String(data: data, encoding: .utf8) else { print("🛑 Could not get contents for file...") exit(1) } let contentWithoutEmojis = contents.removingEmoji
  10. Where it all began… 🔨 #!/usr/bin/env swift import Foundation //

    First argument is the path to the script if CommandLine.arguments.count != 2 { let argumentCount = CommandLine.arguments.count - 1 print("Expected 1 argument but got: \(argumentCount) instead...") exit(1) } let pathToFile = CommandLine.arguments[1] guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(pathToFile)"), let contents = String(data: data, encoding: .utf8) else { print("🛑 Could not get contents for file...") exit(1) } let contentWithoutEmojis = contents.removingEmoji let timeIntervalInMinutes = Double(count(wordsIn: contentWithoutEmojis)) / Double(200) let timeIntervalInSeconds = round(timeIntervalInMinutes * 60)
  11. Where it all began… 🔨 #!/usr/bin/env swift import Foundation //

    First argument is the path to the script if CommandLine.arguments.count != 2 { let argumentCount = CommandLine.arguments.count - 1 print("Expected 1 argument but got: \(argumentCount) instead...") exit(1) } let pathToFile = CommandLine.arguments[1] guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(pathToFile)"), let contents = String(data: data, encoding: .utf8) else { print("🛑 Could not get contents for file...") exit(1) } let contentWithoutEmojis = contents.removingEmoji let timeIntervalInMinutes = Double(count(wordsIn: contentWithoutEmojis)) / Double(200) let timeIntervalInSeconds = round(timeIntervalInMinutes * 60) print(formattedString(from: timeIntervalInSeconds))
  12. Can we make it better? 📈 Not very scalable 🤷

    Only Foundation APIs can be used 🚨 System needs to have swift installed to run the script 📦 Need a better way to distribute it…
  13. Let’s create a Swift Package // swift-tools-version: 5.6 import PackageDescription

    let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library( name: "ReadingTime", targets: ["ReadingTime"] ) ], dependencies: [ ], targets: [ .target( name: "ReadingTime", dependencies: []), .testTarget( name: "ReadingTimeTests", dependencies: ["ReadingTime"], resources: [.copy("MockData")] ), ] )
  14. Let’s create a Swift Package // swift-tools-version: 5.6 import PackageDescription

    let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library( name: "ReadingTime", targets: ["ReadingTime"] ) ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main") ], targets: [ .target( name: "ReadingTime", dependencies: []), .testTarget( name: "ReadingTimeTests", dependencies: ["ReadingTime"], resources: [.copy("MockData")] ), ] )
  15. Let’s create a Swift Package // swift-tools-version: 5.6 import PackageDescription

    let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library( name: "ReadingTime", targets: ["ReadingTime"] ) ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .testTarget( name: "ReadingTimeTests", dependencies: ["ReadingTime"], resources: [.copy("MockData")] ), ] )
  16. Moving the logic into the library import Foundation public enum

    ReadingTime { public static func calculate(for content: String, wpm: Int = 200) -> TimeInterval { } }
  17. Moving the logic into the library import Foundation public enum

    ReadingTime { public static func calculate(for content: String, wpm: Int = 200) -> TimeInterval { let rewrittenMarkdown = MarkdownRewriter(text: content) .rewrite() } }
  18. Moving the logic into the library import Foundation public enum

    ReadingTime { public static func calculate(for content: String, wpm: Int = 200) -> TimeInterval { let rewrittenMarkdown = MarkdownRewriter(text: content) .rewrite() let contentWithoutEmojis = rewrittenMarkdown.removingEmoji } }
  19. Moving the logic into the library import Foundation public enum

    ReadingTime { public static func calculate(for content: String, wpm: Int = 200) -> TimeInterval { let rewrittenMarkdown = MarkdownRewriter(text: content) .rewrite() let contentWithoutEmojis = rewrittenMarkdown.removingEmoji let timeIntervalInMinutes = Double(count(wordsIn: contentWithoutEmojis)) / Double(wpm) let timeIntervalInSeconds = timeIntervalInMinutes * 60 return round(timeIntervalInSeconds) } }
  20. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]) ] ) Adding an executable target
  21. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget( name: "ReadingTimeCLI", dependencies: ["ReadingTime"] ) ] ) Adding an executable target
  22. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.4") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget( name: "ReadingTimeCLI", dependencies: ["ReadingTime"] ) ] ) Adding an executable target
  23. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.4") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget( name: "ReadingTimeCLI", dependencies: ["ReadingTime", .product(name: "ArgumentParser", package: "swift-argument-parser")] ) ] ) Adding an executable target
  24. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), .executable(name: "ReadingTimeCLI", targets: ["ReadingTimeCLI"]) ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.4") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget( name: "ReadingTimeCLI", dependencies: ["ReadingTime", .product(name: "ArgumentParser", package: "swift-argument-parser")] ) ] ) Adding an executable target
  25. Writing some code! import Foundation import ArgumentParser @main struct ReadingTimeCLI:

    ParsableCommand { @Argument(help: "The path to a markdown file to be analysed. Relative to the executable.") var path: String }
  26. Writing some code! import Foundation import ArgumentParser @main struct ReadingTimeCLI:

    ParsableCommand { @Argument(help: "The path to a markdown file to be analysed. Relative to the executable.") var path: String func run() throws { } }
  27. Writing some code! import Foundation import ArgumentParser @main struct ReadingTimeCLI:

    ParsableCommand { @Argument(help: "The path to a markdown file to be analysed. Relative to the executable.") var path: String func run() throws { guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(path)"), let contents = String(data: data, encoding: .utf8) else { throw "🛑 Could not get contents for file..." } } }
  28. Writing some code! import Foundation import ArgumentParser import ReadingTime @main

    struct ReadingTimeCLI: ParsableCommand { @Argument(help: "The path to a markdown file to be analysed. Relative to the executable.") var path: String func run() throws { guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(path)"), let contents = String(data: data, encoding: .utf8) else { throw "🛑 Could not get contents for file..." } } }
  29. Writing some code! import Foundation import ArgumentParser import ReadingTime @main

    struct ReadingTimeCLI: ParsableCommand { @Argument(help: "The path to a markdown file to be analysed. Relative to the executable.") var path: String func run() throws { guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(path)"), let contents = String(data: data, encoding: .utf8) else { throw "🛑 Could not get contents for file..." } let timeInterval = ReadingTime.calculate(for: contents) } }
  30. Writing some code! import Foundation import ArgumentParser import ReadingTime @main

    struct ReadingTimeCLI: ParsableCommand { @Argument(help: "The path to a markdown file to be analysed. Relative to the executable.") var path: String func run() throws { guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(path)"), let contents = String(data: data, encoding: .utf8) else { throw "🛑 Could not get contents for file..." } let timeInterval = ReadingTime.calculate(for: contents) let dateFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.unitsStyle = .full formatter.allowedUnits = [.minute, .second] return formatter }() } }
  31. import Foundation import ArgumentParser import ReadingTime @main struct ReadingTimeCLI: ParsableCommand

    { @Argument(help: "The path to a markdown file to be analysed. Relative to the executable.") var path: String func run() throws { guard let cwd = URL(string: FileManager.default.currentDirectoryPath), let data = FileManager.default.contents(atPath: "\(cwd.path)/\(path)"), let contents = String(data: data, encoding: .utf8) else { throw "🛑 Could not get contents for file..." } let timeInterval = ReadingTime.calculate(for: contents) let dateFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.unitsStyle = .full formatter.allowedUnits = [.minute, .second] return formatter }() print(dateFormatter.string(from: timeInterval) ?? "\(timeInterval) seconds") } } Writing some code!
  32. Compiling for ubuntu 😥 Not as easy as the macOS

    compilation ✅ Cross compilation is an option 💻 Build for Linux but on macOS 📂 Need a destination.json ⛓ Need the target sdk and toolchain 💁 Pro tip: Don’t do it yourself!
  33. Compiling for ubuntu 🔗 https://github.com/keith/swiftpm- linux-cross ⬇ Installs all required

    SDKs 📂 Creates destination.json 🤩 Gives you a swift build command you can copy and paste ⚠ Version of swift for toolchains must match the build version of Swift
  34. Compiling for ubuntu 📂 read-time 📂 toolchain-focal-arm64-5.6.2 destination_static.json destination.json 📂

    packages 📂 swift-5.6.2-RELEASE-ubuntu20.04-aarch64 setup-swiftpm-toolchain --ubuntu-release focal --arch arm64 --swift-version 5.6.2 swift build --destination ~/read-time/toolchain-focal-arm64-5.6.2/destination.json swift build --static-swift-stdlib --destination ~/read-time/toolchain-focal-arm64-5.6.2/ destination_static.json
  35. Compiling for ubuntu private static func count(wordsIn string: String) ->

    Int { var count = 0 let range = string.startIndex..<string.endIndex string.enumerateSubstrings( in: range, options: [ .byWords, .substringNotRequired, .localized ], { _, _, _, _ -> () in count += 1 }) return count } 🛑 Some APIs are not available in all toolchains 👉 Compiler directives can help with this
  36. Compiling for ubuntu #if !os(Linux) private static func count(wordsIn string:

    String) -> Int { var count = 0 let range = string.startIndex..<string.endIndex string.enumerateSubstrings( in: range, options: [ .byWords, .substringNotRequired, .localized ], { _, _, _, _ -> () in count += 1 }) return count } #else private static func count(wordsIn string: String) -> Int { let chararacterSet = CharacterSet .whitespacesAndNewlines .union(.punctuationCharacters) let components = string .components(separatedBy: chararacterSet) let words = components.filter { !$0.isEmpty } return words.count } #endif 🛑 Some APIs are not available in all toolchains 👉 Compiler directives can help with this 🟰 The Windows build faces the same issues 🧐 If you want to learn more about it, I have an article on it.
  37. Compiling for windows 🛑 Docker and cross-compilations did not work

    for me. 🔗 Resorted to a Github Action by Saleem Abdulrasool 🏃 Runs in a windows machine 🧰 Sets up the Swift windows toolchain. 🎉 All I had to do was run swift build on it 💁 Again, don’t do it yourself!
  38. https://www.youtube.com/watch?v=bOMQiMxh5Bc 📣 The Browser Company’s Arc is going to be

    available on Windows next year. 💻 It is macOS only and written in Swift. 💪 They decided to share the same codebase and use Swift for the Windows build. 👨💻 Many challenges ahead and a lot of work to be done to set up the Windows dev environment (Xcode, Instruments, async/await, etc.). 🎉 Could be great for the community and they will potentially open source the work.
  39. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]) ] ) Let’s create a new target
  40. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget(name: "ReadingTimeLambda", dependencies: [ ]) ] ) Let’s create a new target
  41. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget(name: "ReadingTimeLambda", dependencies: [ ]) ] ) Let’s create a new target
  42. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget(name: "ReadingTimeLambda", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") ]) ] ) Let’s create a new target
  43. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget(name: "ReadingTimeLambda", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime") ]) ] ) Let’s create a new target
  44. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .executableTarget(name: "ReadingTimeLambda", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), "ReadingTime" ]) ] ) Let’s create a new target
  45. Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>)

    -> Void ) in guard request.context.http.method == .POST, request.context.http.path == "/calculate" else { return callback(.success(APIGateway.V2.Response(statusCode: .notFound))) } } Writing a lambda!
  46. Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>)

    -> Void ) in guard request.context.http.method == .POST, request.context.http.path == "/calculate" else { return callback(.success(APIGateway.V2.Response(statusCode: .notFound))) } let request = try! jsonDecoder.decode(Request.self, from: request.body ?? "") } Writing a lambda!
  47. Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>)

    -> Void ) in guard request.context.http.method == .POST, request.context.http.path == "/calculate" else { return callback(.success(APIGateway.V2.Response(statusCode: .notFound))) } do { let request = try jsonDecoder.decode(Request.self, from: request.body ?? "") } catch { print(error.localizedDescription) callback(.success(APIGateway.V2.Response(statusCode: .badRequest))) } } Writing a lambda!
  48. Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>)

    -> Void ) in guard request.context.http.method == .POST, request.context.http.path == "/calculate" else { return callback(.success(APIGateway.V2.Response(statusCode: .notFound))) } do { let request = try jsonDecoder.decode(Request.self, from: request.body ?? "") let readingTime = ReadingTime.calculate(for: request.content) } catch { print(error.localizedDescription) callback(.success(APIGateway.V2.Response(statusCode: .badRequest))) } } Writing a lambda!
  49. Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>)

    -> Void ) in guard request.context.http.method == .POST, request.context.http.path == "/calculate" else { return callback(.success(APIGateway.V2.Response(statusCode: .notFound))) } do { let request = try jsonDecoder.decode(Request.self, from: request.body ?? "") let readingTime = ReadingTime.calculate(for: request.content) let body = try jsonEncoder.encodeAsString(Response(time: readingTime)) callback(.success(APIGateway.V2.Response( statusCode: .ok, headers: ["content-type": "application/json"], body: body ))) } catch { print(error.localizedDescription) callback(.success(APIGateway.V2.Response(statusCode: .badRequest))) } } Writing a lambda!
  50. Deploy it! 🚀 # Compile using a docker instance docker

    run \ --rm \ --volume "$(pwd)/:/src" \ --workdir "/src/" \ swift:5.6-amazonlinux2 \ swift build --product ReadingTimeLambda \ -c release -Xswiftc -static-stdlib
  51. Deploy it! 🚀 # Compile using a docker instance docker

    run \ --rm \ --volume "$(pwd)/:/src" \ --workdir "/src/" \ swift:5.6-amazonlinux2 \ swift build --product ReadingTimeLambda \ -c release -Xswiftc -static-stdlib # Package into a `.zip` file for upload # Script from the swift-server repo ./package.sh
  52. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]) ] ) Let’s create a new target, again!
  53. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]) ] ) Let’s create a new target, again!
  54. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https://github.com/JohnSundell/Plot.git", from: "0.5.0") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]) ] ) Let’s create a new target, again!
  55. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https://github.com/JohnSundell/Plot.git", from: "0.5.0") ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .target( name: "ReadingTimeSite", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Plot", package: "plot"), "ReadingTime" ], swiftSettings: [ .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) ] ) ] ) Let’s create a new target, again!
  56. // swift-tools-version: 5.6 import PackageDescription let package = Package( name:

    "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-markdown.git", branch: "main"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https://github.com/JohnSundell/Plot.git", from: "0.5.0"), ], targets: [ .target(name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")]), .target( name: "ReadingTimeSite", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Plot", package: "plot"), "ReadingTime" ], swiftSettings: [ .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) ] ), .executableTarget(name: "ReadingTimeSiteRunner", dependencies: ["ReadingTimeSite"]) ] ) Let’s create a new target, again!
  57. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in } }
  58. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( ) } }
  59. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ) ) } }
  60. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( ) ) } }
  61. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app") ) ) ) } }
  62. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ) ) ) ) } }
  63. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( ) ) ) ) } }
  64. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( .method(.post), .id("playground"), ) ) ) ) } }
  65. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( .method(.post), .id("playground"), .textarea( .placeholder("Enter your markdown here!"), .name("content"), .attribute(named: "form", value: "playground") ) ) ) ) ) } }
  66. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( .method(.post), .id("playground"), .textarea( .placeholder("Enter your markdown here!"), .name("content"), .attribute(named: "form", value: "playground") ), .h3("Estimated Reading Time: 🤷") ) ) ) ) } }
  67. Let’s write the website! func routes(_ app: Application) throws {

    app.get { request async throws -> HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( .method(.post), .id("playground"), .textarea( .placeholder("Enter your markdown here!"), .name("content"), .attribute(named: "form", value: "playground") ), .h3("Estimated Reading Time: 🤷"), .button(.type(.submit), "Calculate Reading Time!") ) ) ) ) } }
  68. Let’s add the missing route! func routes(_ app: Application) throws

    { app.post { request async throws -> Response in } }
  69. Let’s add the missing route! func routes(_ app: Application) throws

    { app.post { request async throws -> Response in let formData = try request.content.decode(FormData.self) } }
  70. Let’s add the missing route! func routes(_ app: Application) throws

    { app.post { request async throws -> Response in let formData = try request.content.decode(FormData.self) let readingTime = ReadingTime.calculate(for: formData.content) } }
  71. Let’s add the missing route! func routes(_ app: Application) throws

    { app.post { request async throws -> Response in let formData = try request.content.decode(FormData.self) let readingTime = ReadingTime.calculate(for: formData.content) let queryParam: String if let readingTimeString = dateComponentsFormatter.string(from: readingTime) { queryParam = "?readingTime=\(readingTimeString)" } else { queryParam = "" } } }
  72. Let’s add the missing route! func routes(_ app: Application) throws

    { app.post { request async throws -> Response in let formData = try request.content.decode(FormData.self) let readingTime = ReadingTime.calculate(for: formData.content) let queryParam: String if let readingTimeString = dateComponentsFormatter.string(from: readingTime) { queryParam = "?readingTime=\(readingTimeString)" } else { queryParam = "" } return request.redirect(to: "/\(queryParam)") } }
  73. Let’s write the website! app.get { request async throws ->

    HTML in return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( .method(.post), .id("playground"), .textarea( .placeholder("Enter your markdown here!"), .name("content"), .attribute(named: "form", value: "playground") ), .h3("Estimated Reading Time: 🤷"), .button(.type(.submit), "Calculate Reading Time!") ) ) ) ) }
  74. Let’s write the website! app.get { request async throws ->

    HTML in let readingTime = try? request.query.get(String.self, at: "readingTime") return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( .method(.post), .id("playground"), .textarea( .placeholder("Enter your markdown here!"), .name("content"), .attribute(named: "form", value: "playground") ), .h3("Estimated Reading Time: 🤷"), .button(.type(.submit), "Calculate Reading Time!") ) ) ) ) }
  75. Let’s write the website! app.get { request async throws ->

    HTML in let readingTime = try? request.query.get(String.self, at: "readingTime") return HTML( .head( .title("Reading Time Playground!"), .stylesheet("https://unpkg.com/@picocss/pico@latest/css/pico.min.css"), .stylesheet("index.css") ), .body( .div( .class("app"), .h1( "Reading Time Playground!" ), .form( .method(.post), .id("playground"), .textarea( .placeholder("Enter your markdown here!"), .name("content"), .attribute(named: "form", value: "playground") ), .if(readingTime != nil, .h3("Estimated Reading Time: \(readingTime ?? "")")), .button(.type(.submit), "Calculate Reading Time!") ) ) ) ) }
  76. Let’s consider a HTML file <!DOCTYPE html> <html lang="en"> <head>…</head>

    <body> <script type="module" src="main.js" defer></script> <div id="app"> <h1>Test ReadingTime from Javascript!</h1> <textarea id="reading-time-content" placeholder="Enter your markdown here!"> </textarea> <button id="reading-time-calculate"> Calculate! </button> </div> </body> </html>
  77. WebAssembly is the 🔑! 🕸 A binary format compatible with

    web technologies. 🧰 Can be compiled from languages such as Swift, Go, Rust etc. 📂 Produces a .wasm fi le ⬇ .wasm fi les can be imported from Javascript. ✅ The WebAssembly JS api is widely supported. https://webassembly.org/
  78. How to compile Swift code to wasm? 👐 SwiftWasm is

    Open Source and provides a set of tools to compile Swift to Wasm. 🧰 Forked swift toolchain to add Wasm compatibility. ⚙ Executable targets can be built to produce wasm binaries. 📚 There is an amazing book available at: https:// book.swiftwasm.org
  79. Using JavascriptKit 📚 JavascriptKit is a library part of the

    SwiftWasm organisation. ✅ Can interact with Javascript through WebAssembly and Swift. 📦 Will make use of https:// github.com/swiftwasm/carton to generate the wasm binary and the needed JS ‘glue’ code. 📖 Read the values from the vanilla JS site.
  80. Let’s go back to our Package.swift // swift-tools-version: 5.6 import

    PackageDescription let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]) ], dependencies: [ ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")] ) ] )
  81. Let’s go back to our Package.swift // swift-tools-version: 5.6 import

    PackageDescription let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]) ], dependencies: [ ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")] ), .executableTarget( name: "ReadingTimeWasm", dependencies: [ ] ) ] )
  82. Let’s go back to our Package.swift // swift-tools-version: 5.6 import

    PackageDescription let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]) ], dependencies: [ .package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.15.0") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")] ), .executableTarget( name: "ReadingTimeWasm", dependencies: [ ] ) ] )
  83. Let’s go back to our Package.swift // swift-tools-version: 5.6 import

    PackageDescription let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]) ], dependencies: [ .package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.15.0") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")] ), .executableTarget( name: "ReadingTimeWasm", dependencies: [ .product(name: "JavaScriptKit", package: "JavaScriptKit") ] ) ] )
  84. Let’s go back to our Package.swift // swift-tools-version: 5.6 import

    PackageDescription let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]) ], dependencies: [ .package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.15.0") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")] ), .executableTarget( name: "ReadingTimeWasm", dependencies: [ .product(name: "JavaScriptKit", package: "JavaScriptKit"), "ReadingTime" ] ) ] )
  85. Let’s go back to our Package.swift // swift-tools-version: 5.6 import

    PackageDescription let package = Package( name: "ReadingTime", platforms: [.iOS(.v8), .macOS(.v12)], products: [ .library(name: "ReadingTime", targets: ["ReadingTime"]), .executable(name: "ReadingTimeWasm", targets: ["ReadingTimeWasm"]) ], dependencies: [ .package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.15.0") ], targets: [ .target( name: "ReadingTime", dependencies: [.product(name: "Markdown", package: "swift-markdown")] ), .executableTarget( name: "ReadingTimeWasm", dependencies: [ .product(name: "JavaScriptKit", package: "JavaScriptKit"), "ReadingTime" ] ) ] )
  86. Let’s write some code! import JavaScriptKit import ReadingTime let document

    = JSObject.global.document var readingTimeButton = document .querySelector("#reading-time-calculate")
  87. Let’s write some code! import JavaScriptKit import ReadingTime let document

    = JSObject.global.document var readingTimeButton = document .querySelector("#reading-time-calculate") readingTimeButton.onclick = .object(JSClosure { _ in })
  88. Let’s write some code! import JavaScriptKit import ReadingTime let document

    = JSObject.global.document var readingTimeButton = document .querySelector("#reading-time-calculate") readingTimeButton.onclick = .object(JSClosure { _ in let element = document .querySelector("#reading-time-content") })
  89. Let’s write some code! import JavaScriptKit import ReadingTime let document

    = JSObject.global.document var readingTimeButton = document .querySelector("#reading-time-calculate") readingTimeButton.onclick = .object(JSClosure { _ in let element = document .querySelector("#reading-time-content") let text = element.value.string })
  90. Let’s write some code! import JavaScriptKit import ReadingTime let document

    = JSObject.global.document var readingTimeButton = document .querySelector("#reading-time-calculate") readingTimeButton.onclick = .object(JSClosure { _ in let element = document .querySelector("#reading-time-content") let text = element.value.string print(ReadingTime.calculate(for: text ?? "")) })
  91. Let’s write some code! import JavaScriptKit import ReadingTime let document

    = JSObject.global.document var readingTimeButton = document .querySelector("#reading-time-calculate") readingTimeButton.onclick = .object(JSClosure { _ in let element = document .querySelector("#reading-time-content") let text = element.value.string print(ReadingTime.calculate(for: text ?? "")) return .null })
  92. What we’ve built using Swift ⚙ A simple script for

    macOS 🍎 A library for all Apple platforms 💻 A command line tool for macOS, Windows and Linux 🔗 An API using AWS lambdas 🛝 An online playground using Vapor + Plot 📦 A wasm binary which can run on the web and anywhere that supports Web Assembly