Bitriseでオーディオ再生をテストする(iOS)

趣味で音楽プレーヤーアプリを作り始めたのでBitriseをセットアップしました。
初回テストを兼ねて最初にオーディオを再生する簡素なコードとテストを書いてCIを動かしたところテストが失敗しました。 (もちろんローカル環境でのテストはパスしている状態です。)

エラーログは下記の通り。

[AudioHAL_Client] AudioHardware.cpp:875:AudioObjectAddPropertyListenerBlock:  AudioObjectAddPropertyListenerBlock: no object with given ID 0
[AudioHAL_Client] AudioHardware.cpp:875:AudioObjectAddPropertyListenerBlock:  AudioObjectAddPropertyListenerBlock: no object with given ID 0
[DDAgg] DefaultDeviceAggregate.cpp:737:BuildAggregate: Error finding valid input or output devices!
[AudioHAL_Client] AudioHardware.cpp:2682:AudioDeviceStop:  AudioDeviceStop: no device with given ID
[aqme] 318: error -66680 finding/initializing AQDefaultDevice
[aurioc] 918: failed: -10851 (enable 2, outf< 2 ch,  44100 Hz, Int16, inter> inf< 2 ch,      0 Hz, Int16, inter>)
[AudioHAL_Client] AudioHardware.cpp:2682:AudioDeviceStop:  AudioDeviceStop: no device with given ID
[aqme] 318: error -66680 finding/initializing AQDefaultDevice
108: * * * NULL AQIONode object
771: Can't make UISound Renderer

有効な入出力デバイスが見つからないと言っていますね。 Bitriseでオーディオ再生を伴うテストをする場合、audio input/output deviceを作る必要があるみたいです。

オーディオデバイスを作成する手順

Homebrewでインストールすることができます。

  1. WorkflowにScript stepを追加
  2. Script contentを記入
#!/usr/bin/env bash
set -ex
brew install Caskroom/cask/soundflower
brew install switchaudio-osx
SwitchAudioSource -s "Soundflower (2ch)" -t input
SwitchAudioSource -s "Soundflower (2ch)" -t output

f:id:tid-a24:20180124053334p:plain

この状態でリビルドすると失敗していたテストケースが全て成功するようになりました。

参考

discuss.bitrise.io

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