In the last post I described how to create a basic Swift command line app that consumed shared Kotlin Multiplatform (KMP) code. Not long after that I watched following recording of Pol Piella’s nice talk at the recent SwiftLeeds conference in which he showed example of using ArgumentParser and ANSITerminal packages in a Swift Command Line app and thought I’d try and use those in another Swift CLI app that consumed KMP code….in this case based on the BikeShare sample.
Did you know Swift is a great language to write your Command Line Tools applications? 🤓@polpielladev talks about that in this talk recorded live at SwiftLeeds this year 🚀
— SwiftLeeds (@swift_leeds) November 9, 2023
Building Delightful Command Line User Experiences with Swift 🖲️https://t.co/yJdmzpA4iw
The following shows the Package.swift
file for the app. We add dependencies in this case for BikeShareSwiftPackage
along
with KMP-NativeCoroutines
and the aforementioned ArgumentParser
and ANSITerminal
packages.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "BikeShare",
platforms: [.macOS(.v13)],
dependencies: [
.package(url: "https://github.com/rickclephas/KMP-NativeCoroutines.git", exact: "1.0.0-ALPHA-18"),
.package(url: "https://github.com/joreilly/BikeShareSwiftPackage", exact: "1.20.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.2.0"),
.package(url: "https://github.com/pakLebah/ANSITerminal", from: "0.0.3")
],
targets: [
.executableTarget(
name: "bikeshare",
dependencies: [
.product(name: "KMPNativeCoroutinesAsync", package: "KMP-NativeCoroutines"),
.product(name: "BikeShareKit", package: "BikeShareSwiftPackage"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "ANSITerminal", package: "ANSITerminal")
]
)
]
)
And the following is the sum total of our Swift code. Due to nature of how we setup the AsyncParsableCommand
, and
it’s use of @main
, this needs to be in separate Swift file (BikeShare.swift
in this case) as opposed to
default main.swift
. The ArgumentParser
package allows us to capture the bike network to use on the command line
and we use ANSITerminal
to allow changing colour of the text outputted based on the bike availability at particular
bike station.
Note that use of AsyncParsableCommand allows us to use Swift async/await code in run()
which in turn allows us to
invoke Kotlin suspend functions directly (using KMPNativeCoroutines).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import ArgumentParser
import KMPNativeCoroutinesAsync
import ANSITerminal
import BikeShareKit
@main
struct BikeShare: AsyncParsableCommand {
@Argument(help: "the bike network")
var network: String
func run() async throws {
KoinKt.doInitKoin()
let repository = CityBikesRepository()
let bikeStations = try await repository.fetchBikeShareInfo(network: network)
bikeStations.forEach { bikeStation in
let freeBikes = bikeStation.freeBikes()
let stationInfo = "\(bikeStation.name): \(freeBikes)"
if (freeBikes < 5) {
print(stationInfo.red)
} else {
print(stationInfo.green)
}
}
}
}
If we build this app then (using swift build -c release
) we can now run it passing the bike network to
use as a command line argument (as shown below for example).