osada-chan

Rails メインでサーバーサイドエンジニアしつつ現場監督やってます

UnsafeMutablePointer で class の property の値の変更を通知を受け取る

以下の記事の続き。 osadake212.hatenablog.com

このアプリでは AudioUnit を使っている。AudioUnit 系の API は以前に比べると Swift で書ける部分も増えてきたが、Objective-CC++ で各部分も多い。Swift とデータのやり取りをするために Swift 側で UnsafeMutablePointer を使うような API が用意されている。

その関係で、仕事では絶対使わないであろう実装を思いついたので共有する。(意味あるんかな)

Swift には stored property に対して willSet や didSet を使って値の監視をすることができる。

struct Setting {
    var version = 1
}

class A {
    var setting: Setting = Setting() {
        willSet {
            print("will set")
        }
        didSet {
            print("did set")
        }
    }
    
    init() {}
}

// ↓ここから呼び出し側の実装

let a = A()
let newSetting = Setting()
a.setting = newSetting

// コンソールに will set/did set が出力されている

まぁこれはいいんだけど、 Setting struct のプロパティが変更された時は通知されない。 この場合だと、例えば

a.setting.version = 2

とかやっても willSet/didSet は呼ばれない。まぁそれはそうか、という感じなんだが、これをなんとかしたかったので UnsafeMutablePointer を使って変更を受け取れるようにしてみた。

struct Setting {
    var version = 1
}

class A {
    var setting: Setting = Setting()
    
    init() {}
    
    func updateSetting(_ handler: (_ setting: UnsafeMutablePointer<Setting>) -> Void) {
        print("will set")
        handler(&setting)
        print("did set")
    }
}

// ↓ここから呼び出し側の実装

let a = A()
let newSetting = Setting()
a.updateSetting { (setting) in
    setting.pointee.version = 2
}
// コンソールに will set/did set が出力されている

print(a.setting.version) // => 2

UnsafeMutablePointer を使うことでインスタンス a のプロパティにアサインされている変数を書き換えることができた。(UnsafeMutablePointer を使わずに渡すと引数の値が変更できない)

これに Observer パターンなどを使ってあげることで、興味があるインスタンスに property の変更を通知することができる。

注意点としては、 Setting のどの部分が変更されたかは通知することができないので、それでも構わない時にしか使えない。 今回は Setting をもとに全ての設定を更新しても、ユーザー体験やパフォーマンスに影響はないケースだったので便利に使えた。

まとめ

Swift にはポインタ型は他にもあり、 AudioUnit では実際 UnsafeMutableRawPointer を使ってたりするのだが、なんかそれっぽい実装を思いついたのでメモしておく。

MPRemoteCommandCenter で AirPods のイベントをハンドリングする

MPRemoteCommandCenter を使うと、ロック画面に出てくるメディアプレイヤーの操作イベントや、 AirPods などの連携している機器からのイベントをハンドリングすることができる。 (以前はもっと別なイベントをハンドリングしてたような気がするが忘れた)

なんでこんな話をしているかというと、以下の記事の続きだからである。

osadake212.hatenablog.com

使い方は簡単で、以下のようにイベントが発生した時の処理を実装できる。

let commandCenter = MPRemoteCommandCenter.shared()
// Play
commandCenter.playCommand.addTarget { (_: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus in
    // TODO: do something
    return .success
}

この例では再生イベントをハンドリングしているが、ハンドリングできるイベントは以下のドキュメントにまとまっている。

https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter

iPhone のロック画面のイベントは名前でなんとなく対応がつくのだが、 AirPods などの機器からどういうイベントが飛んでくるのかは、試して確認したほうがよさそうである。

ちなみに、 MAPlayer ではシークバーのイベントをハンドリングしようとしたのだが、実装上の諸事情により changePlaybackPositionCommand に対して addTarget していない。 なんと、上のコマンドをハンドリングしない場合は、ロック画面のシークバーが無効になった。(よくできてるなぁ、ということが言いたかった。)