XcodeのBuild PhasesでSwiftスクリプトを実行する

ビルド前にファイルの自動生成が必要な時はコンパイル前に行っておく必要があります。
人の温かみを感じるために毎度エンジニアが自動生成するスクリプトを実行しても良いのですが、XcodeのBuild Phasesの中に組み込めばビルド時に自動的に行ってくれます。
Build PhasesではBashスクリプトを実行できます。なので基本Bashスクリプトで自動生成するプログラムを書くのですが、BashからSwiftコマンドを実行すればSwiftで書かれたプログラムを実行できます。

Swiftはコマンドラインの実行も対応しているので、PythonやRubyのスクリプト言語がわからないiOSエンジニアでもメンテナンスができるというメリットがあります。

# 環境
Xcode11.3.1


# スクリプトファイルの作成
空のSwiftファイルを生成します。そして1行目に以下のおまじないを挿入してください。

```Script.swift
#!/usr/bin/swift
```

`#!/usr/bin/swift` はかならず1行目に書きます。コメントの下ではうまく動きません。
これはSwiftコードをコマンドラインから直接実行できるスクリプトであるという宣言になります。
2行目以降は普通のSwiftコードと同じノリで書けます。

```Script.swift
#!/usr/bin/swift

import Foundation

print("hello!")
```

普段UIKitを使っていればあまりimport Foundationすることはないかもしれませんが、Dateを使うには必要なパッケージです。

# Build Phasesに組み込む
まずはこのScript.swiftがBuild Phasesで動くようにします。

Xcodeのプロジェクトを開き、ターゲットのBuild Phasesを開きます。
追加ボタンから「New Run Script Phase」を選択します。
![Screen Shot 2020-03-08 at 16.37.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44992/890c21cf-4397-37d5-afa9-7148ae7c07f6.png)

新たに追加された「Run Script」を展開し、スクリプト欄に以下のコマンドを追加します。パスやファイル名は実際のパスとファイル名を指定してください。
`/usr/bin/env xcrun --sdk macosx swift ${SRCROOT}/CommandSample/Scripts/Script.swift
`
![Screen Shot 2020-03-08 at 17.19.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44992/3923b09f-291c-9b91-5c33-7e29ada34dbf.png)


一度ビルドしてみます。ビルドが終わったらログを見てみましょう。

![Screen Shot 2020-03-08 at 16.46.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44992/90062701-97b3-ef78-8609-3258fd33e839.png)
hello!が出力されていますね！

---

ところで`/usr/bin/env xcrun --sdk macosx swift {Path}`という回りくどいやり方ではなく普通に`swift {Path}`で良いのでは？と思う人もいると思います。
しかしこれではiOSをターゲットとして実行されてしまうのです。環境がiOSになるのでCSVファイルにアクセスする方法が限られてしまうのでmacOSをターゲットとするためにやや回りくどいコマンドとなっています。

macのターミナルから実行する時はターゲットが自動的にmacOSとなっていましたのでやや罠です。

# 引数を受け取る
ファイルを自動生成するにはCSVのパスと出力パスを引数で受け取れたほうが便利です。SwiftではCommandLineから受け取れます。

```Script.swift
#!/usr/bin/swift

import Foundation

// コマンド,引数1,引数2...ととれるので実際に欲しい引数は2番目から。よって最初はDrop
let arguments = CommandLine.arguments.dropFirst()
let inputPath = arguments[0]
let outputPath = arguments[1]

// CSVを読み込む
let csv = try! String(contentsOfFile: inputPath)
// CSVを加工
let output = covertSomething(csv)
// 新しいファイルで保存
try! output.write(toFile: outputPath, atomically: true, encoding: .utf8)
```

スクリプトに引数を追加すれば完了です。
![Screen Shot 2020-03-08 at 17.43.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44992/b153f697-e473-7d2b-01c7-9033ebd05a87.png)
Xcode10のNew Build SystemからBuild Phaseが並列実行するようになるためInput FilesとOutput Filesを指定します。コンパイル前にこのスクリプトが必ず実行されるようになります。

---

スクリプトファイルなのである程度はForce unwrapを使って強気に出てもいいと思いますが、万が一エラーが起きた時にどこでコケたのかを把握しやすくするためにNSError形式でエラーを出したり、適宜print関数を出力すると良いでしょう。

```swift
let inputPath = arguments[0]
let outputPath = arguments[1]

print("引数OK")

if !FileManager.default.fileExists(atPath: inputPath) {
    throw NSError(domain: "Script", code: 1, userInfo: ["description": "CSVがありません。"])
}
```


# [補足]Playgroundでデバッグする
XcodeはコマンドラインSwiftスクリプトの補完がうまくできないので、Xcodeの補完をよく使う人にとっては非常に書きづらいスクリプトです。Swift Playground上であれば補完も効きますし、直接実行してデバッグができます。

Swift Playgroundを始める時にiOSではなくmacOSを選択しましょう。
![Screen Shot 2020-03-08 at 15.38.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44992/e8bcf10d-022d-4ed9-0048-3e62ee05ca4c.png)
Playgroundではコマンドライン引数を渡せませんので、引数から受け取ったあとの処理を試すのがよいでしょう。

```swift
// CSVを読み込む
// let csv = try! String(contentsOfFile: inputPath)

// CSVを読み込んだと仮定
let csv = "A,B,C\n1,2,3"
// CSVを加工
let output = covertSomething(csv)
// できてるか目視
print(output)

// 新しいファイルで保存
// try! output.write(toFile: outputPath, atomically: true, encoding: .utf8)
```