Slide 1

Slide 1 text

Pol Piella Abadia - 📍Do iOS 🇳🇱 - 09/11/22 Fantastic Swift tools and where to find them @polpielladev

Slide 2

Slide 2 text

🧑💻 Senior iOS Engineer @ BBC iPlayer 📍 From Barcelona 🇬🇧 Based in Manchester ❤ Swift and developer tooling 👕 Random fact: I collect football shirts! Hi, I’m Pol 👋

Slide 3

Slide 3 text

🔨 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 🔊

Slide 4

Slide 4 text

⛑ 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

Slide 5

Slide 5 text

My development with other languages https://twitter.com/ThePracticalDev/status/705825638851149824

Slide 6

Slide 6 text

🔧 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 👋

Slide 7

Slide 7 text

Where it all began… 🔨

Slide 8

Slide 8 text

Where it all began… 🔨 #!/usr/bin/env swift

Slide 9

Slide 9 text

Where it all began… 🔨 #!/usr/bin/env swift import Foundation

Slide 10

Slide 10 text

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) }

Slide 11

Slide 11 text

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]

Slide 12

Slide 12 text

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) }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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)

Slide 15

Slide 15 text

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))

Slide 16

Slide 16 text

Let’s run it!

Slide 17

Slide 17 text

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…

Slide 18

Slide 18 text

Let’s create a Swift Package

Slide 19

Slide 19 text

Let’s create a Swift Package swift package init --type library

Slide 20

Slide 20 text

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")] ), ] )

Slide 21

Slide 21 text

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")] ), ] )

Slide 22

Slide 22 text

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")] ), ] )

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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() } }

Slide 25

Slide 25 text

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 } }

Slide 26

Slide 26 text

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) } }

Slide 27

Slide 27 text

But…

Slide 28

Slide 28 text

Adding an executable target

Slide 29

Slide 29 text

// 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

Slide 30

Slide 30 text

// 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

Slide 31

Slide 31 text

// 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

Slide 32

Slide 32 text

// 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

Slide 33

Slide 33 text

// 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

Slide 34

Slide 34 text

Writing some code! import Foundation @main struct ReadingTimeCLI { }

Slide 35

Slide 35 text

Writing some code! import Foundation import ArgumentParser @main struct ReadingTimeCLI: ParsableCommand { }

Slide 36

Slide 36 text

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 }

Slide 37

Slide 37 text

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 { } }

Slide 38

Slide 38 text

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..." } } }

Slide 39

Slide 39 text

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..." } } }

Slide 40

Slide 40 text

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) } }

Slide 41

Slide 41 text

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 }() } }

Slide 42

Slide 42 text

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!

Slide 43

Slide 43 text

Compiling for macOS

Slide 44

Slide 44 text

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!

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Compiling for ubuntu setup-swiftpm-toolchain

Slide 47

Slide 47 text

Compiling for ubuntu setup-swiftpm-toolchain --ubuntu-release focal

Slide 48

Slide 48 text

Compiling for ubuntu setup-swiftpm-toolchain --ubuntu-release focal --arch arm64

Slide 49

Slide 49 text

Compiling for ubuntu setup-swiftpm-toolchain --ubuntu-release focal --arch arm64 --swift-version 5.6.2

Slide 50

Slide 50 text

Compiling for ubuntu setup-swiftpm-toolchain --ubuntu-release focal --arch arm64 --swift-version 5.6.2

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Compiling for ubuntu private static func count(wordsIn string: String) -> Int { var count = 0 let range = string.startIndex.. () in count += 1 }) return count } 🛑 Some APIs are not available in all toolchains 👉 Compiler directives can help with this

Slide 53

Slide 53 text

Compiling for ubuntu #if !os(Linux) private static func count(wordsIn string: String) -> Int { var count = 0 let range = string.startIndex.. () 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.

Slide 54

Slide 54 text

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!

Slide 55

Slide 55 text

Compiling for windows

Slide 56

Slide 56 text

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.

Slide 57

Slide 57 text

✅ ✅ ✅

Slide 58

Slide 58 text

Creating an API

Slide 59

Slide 59 text

// 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

Slide 60

Slide 60 text

// 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

Slide 61

Slide 61 text

// 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

Slide 62

Slide 62 text

// 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

Slide 63

Slide 63 text

// 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

Slide 64

Slide 64 text

// 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

Slide 65

Slide 65 text

Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result) -> Void ) in } Writing a lambda!

Slide 66

Slide 66 text

Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result) -> 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!

Slide 67

Slide 67 text

Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result) -> 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!

Slide 68

Slide 68 text

Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result) -> 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!

Slide 69

Slide 69 text

Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result) -> 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!

Slide 70

Slide 70 text

Lambda.run { ( context, request: APIGateway.V2.Request, callback: @escaping (Result) -> 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!

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

Making an online playground

Slide 75

Slide 75 text

// 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!

Slide 76

Slide 76 text

// 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!

Slide 77

Slide 77 text

// 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!

Slide 78

Slide 78 text

// 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!

Slide 79

Slide 79 text

// 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!

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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") ) ) } }

Slide 83

Slide 83 text

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( ) ) } }

Slide 84

Slide 84 text

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") ) ) ) } }

Slide 85

Slide 85 text

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!" ) ) ) ) } }

Slide 86

Slide 86 text

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( ) ) ) ) } }

Slide 87

Slide 87 text

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"), ) ) ) ) } }

Slide 88

Slide 88 text

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") ) ) ) ) ) } }

Slide 89

Slide 89 text

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: 🤷") ) ) ) ) } }

Slide 90

Slide 90 text

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!") ) ) ) ) } }

Slide 91

Slide 91 text

No content

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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) } }

Slide 94

Slide 94 text

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) } }

Slide 95

Slide 95 text

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 = "" } } }

Slide 96

Slide 96 text

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)") } }

Slide 97

Slide 97 text

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!") ) ) ) ) }

Slide 98

Slide 98 text

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!") ) ) ) ) }

Slide 99

Slide 99 text

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!") ) ) ) ) }

Slide 100

Slide 100 text

No content

Slide 101

Slide 101 text

Calling Swift from Javascript?

Slide 102

Slide 102 text

Let’s consider a HTML file …

Test ReadingTime from Javascript!

Calculate!

Slide 103

Slide 103 text

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/

Slide 104

Slide 104 text

No content

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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.

Slide 107

Slide 107 text

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")] ) ] )

Slide 108

Slide 108 text

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: [ ] ) ] )

Slide 109

Slide 109 text

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: [ ] ) ] )

Slide 110

Slide 110 text

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") ] ) ] )

Slide 111

Slide 111 text

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" ] ) ] )

Slide 112

Slide 112 text

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" ] ) ] )

Slide 113

Slide 113 text

Let’s write some code! import JavaScriptKit import ReadingTime

Slide 114

Slide 114 text

Let’s write some code! import JavaScriptKit import ReadingTime let document = JSObject.global.document

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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 })

Slide 117

Slide 117 text

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") })

Slide 118

Slide 118 text

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 })

Slide 119

Slide 119 text

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 ?? "")) })

Slide 120

Slide 120 text

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 })

Slide 121

Slide 121 text

Bundling… 📦 carton bundle --product ReadingTimeWasm

Slide 122

Slide 122 text

JavascriptKit code Binary Glue code

Slide 123

Slide 123 text

No content

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

A quick shoutout before I go! https://github.com/joshdholtz/DeckUI

Slide 126

Slide 126 text

polpiella.dev @polpielladev Say hi! 👋 iOS CI Newsletter NEW!