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

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

Avatar for Pol Piella Abadia Pol Piella Abadia
November 17, 2022
1.6k

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

Avatar for Pol Piella Abadia

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