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.


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

bikeshare cli