会員事業部の三木(@giginet)です。
この記事では、業務改善のために開発者向けのツールをSwiftで開発してみたため、その知見についてお伝えしたいと思います。
なお、この記事はXcode7.1上でSwift2.1を使った開発を前提としています。
作ったもの
クックパッドiOSアプリでは開発の際に、新しい機能を実装したり、インターフェイスを改善したあとにiOSシミュレーターの動画を撮影しPull Requestに貼り付けています。
動画を撮影する際には、汎用的にスクリーンキャストを撮影する社内ツールを使っていたのですが、使いづらい面も多かったため、 簡単にiOSシミュレーターの操作をアニメーションgifとして記録したいという需要がありました。
そのため、空き時間を使って、簡単なユーティリティを実装しました。
なぜSwiftで作るのか
今回は、OS Xの開発用SDKであるCocoaを使い、直接ウィンドウの描画結果を取得したいという需要があったため、 一般的なコマンドラインツールによく使われているスクリプト言語ではなくSwiftを採用しました。
わざわざSwiftでコマンドラインツールを実装することには以下のような利点があります。
Swiftで実装する利点
OS XのネイティブAPIを直接扱える
最大の利点は、Cocoaの資産を簡単に利用できることです。
Cocoaをスクリプトから利用する方法として、他に、AppleScriptやJavaScript(JXA)、MacRubyなどによるbindingが存在しますが、 どれも言語として書きづらかったり、全ての機能を利用できないこともあります。
高速に動作する
スクリプトと異なり、ネイティブコードとして実行されるため高速で動作します。
バイナリを配布しやすい
バイナリ形式で配付することで、環境構築をせずともそのまま動作します
Objective-Cに比べて開発しやすい
SwiftにはObjective-Cと比べ、REPL環境やPlaygroundが利用でき、検証が容易です。
また記述量も減り、記述しやすい点も挙げられます。
Swiftで作らない方が良い場合
その一方で、採用しづらい理由として、以下のような点が挙げられます。
- Mac以外では動作しない
- SwiftやAPIの仕様が変更される可能性がある
- 開発環境を整えづらい
これらの特徴を理解し、最適な場面でのみ採用することが肝要です。
Swiftで簡単なスクリプトを書く
早速、Swiftで簡単なスクリプトを記述してみましょう。
Swiftで書いたコードはswift
コマンドを利用して、スクリプト言語のように即座に実行することができます。
例えば、スクリプトからCocoaを呼び出し、ダイアログを表示させてみます。
#!/usr/bin/env swiftimport AppKit let alert: NSAlert= NSAlert() alert.messageText ="Do you like cooking?" alert.addButtonWithTitle("Yes") alert.addButtonWithTitle("No") alert.alertStyle = NSAlertStyle.WarningAlertStyle let response: Int= alert.runModal() switch response { case NSAlertFirstButtonReturn: print("Yes") case NSAlertSecondButtonReturn: print("No") default: break }
このような短い実装で、簡単にOSのUIを利用したスクリプトを記述することができます。
エントリーポイントの定義なども必要ありません。
他のスクリプト言語のようにshebangを利用することもでき、あたかもスクリプト言語のようにSwiftのコードを実行することができます。
$ chmod +x alert $ ./alert
パッケージ管理を行う
簡単なスクリプトであれば上記のようにすぐに実装することができますが、ある程度複雑なスクリプトを実装しようとすると、外部ライブラリを利用したいケースが出てくるでしょう。
SwiftでもRubyでいうBundlerのような、依存関係を解決してくれるパッケージマネージャとしてCarthageが存在するので、今回はそれを利用します。
CarthageはSwiftで実装されたパッケージマネージャで、iOS/Macアプリ用のライブラリを管理することができます。
似たようなツールとしてCocoaPodsがよく知られていますが、CocoaPodsはアプリケーションへの組み込みを想定したツールであるため、今回のようなケースで利用することが難しいです。
その一方で、Carthageは設定方法がCocoaPodsより複雑である反面、Xcodeのプロジェクトファイルを用いてビルドや依存関係を解決してるので、シンプルでありコマンドラインツールの開発でも利用しやすいことが特徴です。
つい先日、Swift2.xに対応した最新バージョンがHomebrewにリリースされたため簡単に導入することができます。
コマンドラインオプションを実装する
SwiftではProcess.arguments
からコマンドライン引数を受け取ることができます。
let arguments = Process.arguments.suffixFrom(1) print(arguments)
これは以下のように実行できます。
$ ./arguments I love beer ["I", "love", "beer"]
この状態では、単に文字列として受け取れるだけなので、コマンドラインオプションを自前で実装するには手間がかかります。
Swiftでは他の言語のようにオプションパーサーが標準では用意されていないため、OptionKitやCommandantなどの外部のライブラリを利用する必要があります。
今回はCarthageを使い、OptionKitを導入してみます。
まずプロジェクトにCartfile
を置き、依存関係を定義します。
github "nomothetis/OptionKit" ~> 1.0.0
その後、update
を実行することで、Carthage/Build/Mac
以下にビルド済みのフレームワークが生成されます。
$ carthage update
OptionKitを用いると、以下のように簡単にコマンドラインオプションを実装することができます。
#!/usr/bin/env swift -FCarthage/Build/Macimport OptionKit import CoreFoundation let arguments = Array((Process.arguments[1..<Process.arguments.count])) // Define optionslet frameRateOption = Option(trigger: OptionTrigger.Mixed("f", "fps"), numberOfParameters: 1, helpDescription: "Recording frames per second") let outputPathOption = Option(trigger: OptionTrigger.Mixed("o", "outputPath"), numberOfParameters: 1, helpDescription: "Animation output path") let helpOption = Option(trigger:.Mixed("h", "help")) // Create Parserlet parser = OptionParser(definitions: [frameRateOption, outputPathOption]) // Parse optionsdo { let (options, _) = try parser.parse(arguments) if options[helpOption] !=nil { print(parser.helpStringForCommandName("option-parser")) exit(EXIT_FAILURE) } iflet frameRate: UInt= options[frameRateOption]?.flatMap({ UInt($0) }).first { print(frameRate) } iflet outputPath = options[outputPathOption]?.first { print(outputPath) } } catch let OptionKitError.InvalidOption(description: description) { print(description) exit(EXIT_FAILURE) }
ポイントとして、1行目のshebangを変更して、今フレームワークをインストールしたディレクトリをサーチパスとして追加しています。 これによって、実行時に自動的に依存しているフレームワークを読み込んで実行することができます。
OptionKitではSwift2から実装された例外が利用されており、オプションの取得に失敗すると例外が送出されます。
このスクリプトを実行し、-h
コマンドを呼び出すと、以下のようにオプションの一覧が表示されます。
$ ./option-parser -h $ usage: option-parser [-f|--fps] [-o|--outputPath] [-h|--help]
signalをフックする
最後にシェルからINTERRUPT
やTERMINATE
のシグナルを取得する方法を見てみましょう。
今回はSwiftからCのライブラリであるsignal
を直接呼び出しています。
signal
には引数として関数ポインタを渡す必要がありますが、通常の関数の場合は、以下のようにCの関数ポインタのように扱うことができます。
import CoreFoundation func callback(_: Int32) { print("process killed") exit(EXIT_SUCCESS) } signal(SIGINT, callback) whiletrue { }
クロージャを渡したい場合は少し複雑です。
ここではSwift2から導入された@convention
シンタックスを利用しています。
SwiftではCの関数ポインタの型は@convention
を用いて表されます。
signal
の引数はvoid (*)(int)
型であり、これをSwift2上では@convention(c) (Int32) -> Void
という型で表せます。
その後、Swiftのクロージャを上記で定義した型にキャストし、関数ポインタとして渡します。
import CoreFoundation typealias SignalCallback = @convention(c) (Int32) -> Voidlet callback: @convention(block) (Int32) -> Void = { (Int32) -> Void in print("process killed") exit(EXIT_SUCCESS) } // Convert Objective-C block to C function pointerlet imp = imp_implementationWithBlock(unsafeBitCast(callback, AnyObject.self)) signal(SIGINT, unsafeBitCast(imp, SignalCallback.self)) whiletrue { }
これにより、シェルからのCtrl + C
をフックして、独自の処理を実行することが可能になりました。
今回は動画の撮影終了を検知するのに利用しています。
バイナリとして配付する
あとはCocoaの知見があればコマンドラインツールを簡単に作成することができます。
このままスクリプトとして配布してしまうと、Xcodeを導入したり、開発環境を整えなければスクリプトを実行することができません。 そのため、今回はビルドしてバイナリとして配付してみましょう。
xcodebuild
を利用してプロジェクトファイルからビルドする方法もありますが、今回は構造がシンプルなのでxcrun
を使ってビルドしてみます。
$ xcrun -sdk macosx swiftc main.swift \-FCarthage/Build/Mac \-Xlinker-rpath-Xlinker"@executable_path/../Frameworks/"\-o simrec
外部のフレームワークは実行バイナリに含めることができないため、実行バイナリのパスから相対パスでフレームワークを読むような設定にします。
今回は、実装したフレームワークとバイナリの配布にはHomebrewを用いることを想定して、以下のようなディレクトリ構成でインストールされると仮定します。
├── Frameworks │ └── OptionKit.framework └── bin └── simrec
そのため、実行バイナリであるsimrec
から見て../Frameworks/
に配置された外部フレームワーク(今回はOptionKit)を読めるようにビルドしています。
まとめ
この記事ではSwiftを使ったコマンドラインツールの実装例をご紹介しました。
なかなか知見が少なく取っつきづらい部分も多いですが、お役に立ちましたら幸いです。
また、今回実装したものを公開していますので、ご興味のある方は参照してみてください。