macOS appのCloudKitでカスタムコンテナを使う時に気をつけること

環境:Xcode 8.3.3 / Swift 3.1

追記 デフォルトコンテナとカスタムコンテナが一致している場合はCKContainer.default()が使えて、
一致していない場合はコンテナ名を指定する必要がある、、、ということでした。

問題

macOS appでCloudKitを使う際、Capabilitiesでカスタムコンテナを1つだけ指定してから
CKContainer.default() でレコードを保存しようとすると下記エラーが出ました。

"Server Rejected Request" (15/2001); "Request failed with http status code 500"

コードはこんな感じです。

let database = CKContainer.default().privateCloudDatabase
let record = CKRecord(recordType: "Memo")
record.setValue("Hello!", forKey: "message")
database.save(record) { _, _ in
}

解決方法

どうやらmacOS appでカスタムコンテナを使う場合、
CKContainerインスタンスを作る時にコンテナ名を指定しないといけないようです。

let database = CKContainer(CUSTOM_CONTAINER_NAME).privateCloudDatabase

なお、iOSでカスタムコンテナを使う場合はCKContainer.default()が問題なく使用できます。
勘違いでした。CKContainer.default()だとCapabilitiesのカスタムコンテナの指定は無視され、
Use default containerを指定した場合と同じコンテナが使われます。
macOS appだとデフォルトコンテナが自動作成されないようで、そのためにエラーが発生していました。

リファレンス等で確認できませんでしたが、ハマったのでメモとして残しておきます。

複数のアプリで共通のiCloud Key-Value Storageにアクセスする方法

異なるapp(macOS appとiOS appなど)で共通のiCloud Key-Value Storageを使いたい場合、
どちらかの.entitlementsファイルを編集する必要があります。

具体的にはcom.apple.developer.ubiquity-kvstore-identifierキーの値を共通の値にしなければいけません。

デフォルトの値はどちらも$(TeamIdentifierPrefix)$(CFBundleIdentifier)となっているはずなので、 どちらか片方の値をもう片方の値と同じになるように変更すればOKです。

通常、先に開発したアプリをプライマリとして後発アプリの.entitlementsファイルを編集することになると思います。
同時開発している場合はどちらをプライマリにしても問題ないと思います。

例えば、macOS appの$(CFBundleIdentifier)com.my.appnameだとすると、
iOS appのcom.apple.developer.ubiquity-kvstore-identifierキーの値を
$(TeamIdentifierPrefix)com.my.appnameとすれば共通のiCloud KVSにアクセスするようになります。

参考

developer.apple.com

NSOutlineViewのNSTableCellViewを編集状態にする方法

View basedなNSOutlineViewでダブルクリックされたセルを編集状態にする方法です。

NSOutlineViewを継承してfunc mouseDown(with event: NSEvent)
オーバーライドする必要があります。

class MyOutlineView: NSOutlineView {
    override func mouseDown(with event: NSEvent) {
        super.mouseDown(with: event)

        guard event.clickCount >= 2 else {
            return
        }

        let point = convert(event.locationInWindow, from: nil)
        let selectedRow = row(at: point)

        guard selectedRow != NSOutlineViewDropOnItemIndex else {
            return
        }

        editColumn(0, row: selectedRow, with: nil, select: true)
    }
}

func editColumn(_ column: Int, row: Int, with event: NSEvent?, select: Bool)
呼ぶことで編集状態になります。
このメソッドは指定されたセルが 編集可能(isEditable == true) なら編集状態にしてくれます。

システムアイコンのNSImageを作る方法

Finderで表示されるフォルダーのアイコンなどを作る方法。

let folderIcon: NSImage = NSWorkspace.shared().icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kGenericFolderIcon)))

kGenericFolderIconを変更すれば色々なアイコンを取得できます。

元となるアイコンファイルは下記のパスにあります。

/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources

macOS app FullScreen時にToolbarを隠す

画像のようなToolbarを持つアプリをFullScreenにした時に
Toolbarを隠すにはNSWindowDelegateを設定します。

// NSWindowDelegateを設定
class WindowController: NSWindowController {
    override func windowDidLoad() {
       super.windowDidLoad()
    window?.delegate = self
    }
}

extension WindowController: NSWindowDelegate {
    func window(_ window: NSWindow,
                willUseFullScreenPresentationOptions proposedOptions: NSApplicationPresentationOptions = []) -> NSApplicationPresentationOptions {
        return [
            .fullScreen,
            .hideDock,
            .autoHideMenuBar,
            .autoHideToolbar
        ]
    }
}

隠れたToolbarはマウスカーソルを画面上部に移動させると表示されます。

なお、NSApplicationPresentationOptionsの組合せには制限があります。
正しくない組合せを指定すると落ちることもあるので
上記以外の組合せを試す場合はドキュメントを参照してください。

参考

NSApplicationPresentationOptions - NSApplication | Apple Developer Documentation

指定したURLをFinderで開く

コンテキストメニューでよくみかけるShow in Finderを実装する方法です。

let url: URL! = URL(string: fileOrDirectoryPath)
NSWorkspace.shared().activateFileViewerSelecting([url])

ディレクトリを指定した場合はopenでも開けますが、
ファイルを指定した場合は開けないか、関連付けされているアプリが開いてしまいます。

NSWorkspace.shared().open(url) // NG: Finderを開く用途では使えない

参考

activateFileViewerSelecting(_:) - NSWorkspace | Apple Developer Documentation