Swiftを使ったMac OS Xアプリの開発【コマンド実行編】

**2018/05/23 Swift4 対応**

Swiftに関する日本語の情報もかなり増えてきているように感じますが、殆どがiOSアプリ開発に関するものです。そこで、趣味でMac OS Xアプリ開発を行いましたので、数回に分けて記事にしていこうと思います。
ちなみに、Macの利用比率が世界的に低いため、利益などを考慮する場合は、WebアプリやJavaアプリとして開発したほうがいいかもしれません。また、Swiftで開発を行うと、昔のMac OSで対応できないものが出てくるようです。

## 前提条件
iOSアプリの開発歴があるものとして解説を行いますので、共通な作業は省略している箇所があります。また、アプリに実装する機能などによっては、追加で必要なことや、今回の作業に不要なことが含まれている可能性があります。
ファイルアクセス等に関する公式Referenceはそこそこなボリュームがあり、全てを読み終えておりません。また、英語の読み間違えなども考えられますので、リリースをお考えの方は、一度ご自身でReferenceを読んだ上で参考にしていただければと思います。
※ ディレクトリ名がユーザ名の場合は「AKKEY」と表記しております。

## 開発環境
Mac OS X 10.11.6
Xcode 7.3.1

## 完成作品
<div align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/128344/ecd3c9d4-007b-a6dc-76cb-c61a6540dacb.png" alt="完成作品" title="完成作品" with="300" height="240" />
</div>

アプリ側からmkdirとcpコマンドを実行してみたいと思います。cpコマンドは、ユーザにpng画像を選択してもらい、その画像をアプリデータ管理領域にコピーするために用います。
ソースを[GitHub](https://github.com/AkkeyLab/MacOSAppTraining03)に置いていますので、よければご利用ください。

## Appからmkdir実行
さすがOS Xアプリ！アプリ側からコマンド実行が可能です。
今回はDesktopディレクトリにAkkeyLabという名前のディレクトリを作成するコマンドを実行してみたいと思います。  

~~~Swift
import Cocoa

class ViewController: NSViewController {
    let task = NSTask()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func mkDir(sender: AnyObject) {
        // ホームディレクトリに移動と作成したいディレクトリを追記
        let outputDir: String = NSHomeDirectory() + "/Desktop/AkkeyLab"
        // 検証のために表示させてみましょう
        NSLog("\(outputDir)")
        // 起動するプログラムを絶対パスで指定
        task.launchPath = "/bin/mkdir"
        // オプションを指定
        task.arguments = [outputDir]
        // コマンド実行
        task.launch()
    }

    override var representedObject: AnyObject? {
        didSet {
            // Update the view, if already loaded.
        }
    }
}
~~~

NSHomeDirectoryを使用することで、アプリケーションから見えるホームディレクトリを取得することが出来ます。今回は、そこに追加する形で作成するディレクトリを記述しています。
注意すべき箇所としては、mkdirを絶対パスで指定して上げる必要があるところです。日頃、シェルに助けられていることを実感します。  
>2016-09-01 00:37:02.488 Training[17569:10690419] **/Users/AKKEY/Desktop/AkkeyLab**

実行するとDesktopにディレクトリが作成されたと思います。（ディレクトリをLog出力させていると上記のような感じで出力があるはずです）
再度実行してみましょう。既に実行済みのタスクであると怒られてしまいます。どうやら、何度もできるわけではないようです。  

## リリースを考えた場合のmkdir実行
実はですね。これだけのコードだとリリースできません。
もしこれでリリース可能だったらと想像するだけでゾッとします。
試しに、リリースできる環境にして再度実行してみることにしましょうか。

<div align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/128344/4d7f8ca3-38bb-57f8-6aae-7b8415e261f1.png" alt="設定項目" title="設定項目" with="300" height="230" />
</div>

Projectファイルを選択して、 **Capabilities** から **Sandbox** をONにしてください。下に、複数の設定項目がありますが、初期のままにしておきたいと思います。
では、先ほどのコードのままで再度実行してみてください。  

>2016-09-01 00:00:20.507 Training[14694:10619359]  **/Users/AKKEY/Library/Containers/com.akkeylab.Training/Data/Desktop/AkkeyLab
mkdir** : /Users/AKKEY/Library/Containers/com.akkeylab.Training/Data/Desktop/AkkeyLab: Operation not permitted

こんな感じのメッセージが表示され、Desktopにディレクトリが作成されることはありませんでした。
そこで、エラーコードを見てみてください。
「あれ？」ってなりませんか？
実行できた時と指定ディレクトリが異なっているんです。
そう！これがSandboxの **力** なんです！！
詳しいことは英語の公式Referenceを見ていただくとして、App側が使用可能な保存領域が確保されたり、アクセスできる場所に制限がついたりするんです。  

>/Users/AKKEY/Library/Containers/com.akkeylab.Training/Data/

Sandbox利用時は、ホームディレクトリが上記になります（初期設定によって一部名称が変わります）。

<div align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/128344/188c44ee-511c-6b9c-506a-651d72362a42.png" alt="確認" title="確認" with="300" height="300" />
</div>

では、今回のPermissionエラーの対策をしましょう。
上記の画像を見ると、Desktopへのエイリアスがあることがわかります。ですが、FileAccessの設定項目にDesktopが無いため、別の方法で許可させて上げる必要があるようです。
現時点で、アクセス許可方法がわからないため、ホームディレクトリに「AkkeyLab」ディレクトリを作成するようにコードを変更して実行してみました。
上の画像のように、しっかりとディレクトリが作成されていれば成功です！  

## Appからcp実行
次に、ちょっと実践的なことをしてみたいと思います。
ユーザが指定したファイルをApp側のホームディレクトリにコピーしたいと思います。

### 1.ファイル選択の有効化
Sandboxに切り替えた際に、スルーしていた設定項目を変更する必要があります。
FileAccessの設定項目にある「User Selected File」を「Read Only」に変更します。書き込み権限を同時に与えることも可能ですが、セキュリティ上よろしいことではありません。

### 2.プログラム作成
~~~Swift
@IBAction func cp(sender: AnyObject) {
    let panel = NSOpenPanel()
    panel.canChooseFiles = true
    panel.canChooseDirectories = false
    panel.allowsMultipleSelection = false
    // 選択対象を画像だけに指定
    panel.allowedFileTypes = NSImage.imageTypes
    panel.beginWithCompletionHandler({ (num) -> Void in
        if num == NSApplication.ModalResponse.OK {
            // 画像のパスを引数にメソッドを呼び出す
            self.readItem((panel.URL?.path)!)
        }
   })
}

func readItem(url: String) {
    if (NSImage(contentsOfFile: url) != nil) {
        task.launchPath = "/bin/cp"
        // オプションは、コマンド入力時と同じ順番で配列に格納
        task.arguments = [url, homeDir + "/AkkeyLab/sample.png"]
        task.launch()
    } else {
        alert.messageText = "Error"
        alert.informativeText = "Not find file"
        alert.runModal()
    }
}
~~~

上記プログラムを追記してください。
NSOpenPanelに関しては、以下の記事で紹介しております。プログラムコード上の説明で重複する内容は省いておりますので、以下の記事も参考にしてみてください。
[Swiftを使ったMac OS Xアプリの開発【Data Road偏】](http://qiita.com/AkkeyLab/items/059e7223434270266c3d)

### 3.実行結果
<div align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/128344/0ee70ddc-75da-84f9-aa69-81a4e318fe51.png" alt="結果" title="結果" with="300" height="300" />
</div>

AkkeyLabディレクトリ内にsample.pngが作成されていることが確認できました。

## 最後に
OS X（macOS）アプリでSandboxに関係する機能や操作を実装する際に、開発者として以下は読んでおくべきでしょう。
[App Sandbox Design Guide](https://developer.apple.com/library/mac/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW4)
今後も記事にしていく予定ですので、よろしくお願いします。