Swift から Method Swizzling

MacDev

昔はよく使ってた Method Swizzling だけど、Objective-C ランタイムの機能なので Swift 時代になってからはなんとなく手を出さずにいた。でも実際に書いてみると何も import せずに関連関数を呼び出せたし、普通にすっきりしたコードで実現できるのね。

extension NSObject
{
    public class func swizzle(_ originalSelector: Selector, to replacedSelector: Selector)
    {
        method_exchangeImplementations(
            class_getInstanceMethod(self, originalSelector)!,
            class_getInstanceMethod(self, replacedSelector)!
        )
    }
}
少し便利な定義
extension NSCursor
{
    @objc public func swizzle_set()
    {
        //call original method
        swizzle_set()

        if (self == .arrow), NSEvent.modifierFlags.contains(.shift) {
            NSLog("\(self) set! \(Thread.callStackSymbols) \(NSApp.currentEvent)")
        }
    }
}

NSCursor.swizzle(#selector(NSCursor.set), to: #selector(NSCursor.swizzle_set))
使用例

例えばカーソルの変更みたいに頻繁に呼ばれる処理を調べたい場合、Xcode のデバッグ機能でシンボル -[NSCursor set] をトリガーに指定して止めるやり方では必要なタイミングに限定しにくいけど、Method Swizzling を使用すると「対象が黒矢印カーソルのとき」「Shift キーが押されているとき」のように好きな条件で発動させることができて簡単だ。

多用していたあの頃は SIMBL プラグインを作るという目的があったから特定の接頭辞のついた大量のメソッドを一括で置き換えるような処理を書いたりもしていたけど、単純にアプリケーションのバグ原因を調査する目的1であればそこまで必要ないでしょう。


  1. 実際は悩んだ末にここまでやってたどり着くのが AppKit 側のバグだったりするのが泣ける。 ↩︎

スペースバーを押したときだけ手のひらツールにする

MacDev

「スペースバーが押されているあいだだけ手のひらツールに変化1させるのが大変だった」という話を耳にしたので自分も挑戦してみた。もっときれいな方法や間違いがあったら教えてください。

→ 続きを読む


  1. 広く見かける UI だけど、現在の Apple 純正ソフトウェアで使えるのは Keynote ぐらい? ↩︎

Swift の KeyPath を使った Binding

MacDev

Cocoa Bindings は Objective-C 時代にできたものだから String でプロパティのキーパスを指定するけど、 Swift 4 の KeyPath と Generics を組み合わせればすっきりしたコードで書けるのではないかと思いついたので実験。

protocol KeyPathBasedBinding {}
extension KeyPathBasedBinding
{
    /// When the returned object is deinited or invalidated, it will stop binding.
    /// - Parameter targetKeyPath: The KeyPath for target property. Target property must be observable through KVO.
    func bind<Value, Target>(_ keyPath: ReferenceWritableKeyPath<Self, Value>, to targetKeyPath: KeyPath<Target, Value>, of target: Target) -> NSKeyValueObservation where Self: AnyObject, Target: _KeyValueCodingAndObserving
    {
        return target.observe(targetKeyPath, options: [.initial, .new], changeHandler: { [weak self] (sender, change) in
            self?[keyPath: keyPath] = target[keyPath: targetKeyPath]
        })
    }

    /// When the returned object is deinited or invalidated, it will stop binding.
    /// - Parameter targetKeyPath: The KeyPath for target property. Target property must be observable through KVO.
    func bind<Value, Target>(_ keyPath: ReferenceWritableKeyPath<Self, Value>, to targetKeyPath: KeyPath<Target, Optional<Value>>, of target: Target, nullValue: Value) -> NSKeyValueObservation where Self: AnyObject, Target: _KeyValueCodingAndObserving
    {
        return target.observe(targetKeyPath, options: [.initial, .new], changeHandler: { [weak self] (sender, change) in
            self?[keyPath: keyPath] = (target[keyPath: targetKeyPath] ?? nullValue)
        })
    }
}
extension NSObject: KeyPathBasedBinding {}

Cocoa Bindings だと bind(_,to:,withKeyPath:,options) の第一引数には自身のプロパティのキーもしくは特別に用意された NSBindingName の定数を使用することができるけど、今回再現しているのは前者だけだ。

→ 続きを読む

NSVisualEffectView の角を丸くする 2 つの方法

MacDev

画面の端にぴったりくっつけることが多い iOS と違い、macOS だと余白を設けてビューを配置することが多い。そうすると角を丸くしたくなる。木工家具でやすりがけや面取りをするような基本的な話だ。

AppKit の標準コントロールのほとんどはそういう仕上げがしてあるから意識する必要はないのだけど、先日 NSVisualEffectView の角を丸くしたい状況があったのでやり方を考えてみた。

→ 続きを読む

NSAppearance.current の役割

MacDev

なぜか天気予報の全国図で一番気温が高い日が多く 39℃ が当然のように何日も続いていた名古屋にもやっと秋が来た1。秋といえば Apple プラットフォーム開発者が忙しい季節。例によって iOS 12 の話は置いておくとして、ここで取り上げる話題はもちろん macOS Mojave についてだ。

Mojave といえばやはり目玉はダークモード2。WWDC 2018 の関連セッションビデオを見ると NSColor に追加されたシステムカラーの説明に重きが置かれている。

ユーザがいつでもモードを変更できるためこれらのカラーはそれに応じてダイナミックに変化するようになっていて、例えば NSColor.controlBackgroundColor はダークモードで描画すると暗いグレー、そうでなければホワイトとして描画される。

でもどうやって判別しているのか。システム全体が非ダークモード(ライトモード?)であってもダークなウインドウを混ぜることができるし、ライトなウインドウの中に一部だけダークなビューを混ぜることもできてしまう。そのためシステムの設定を取得しても意味がない。

そこで使用されているのが NSAppearance にある current というクラスプロパティ。NSView は draw(_:) とか updateLayer() が呼び出される直前にこれをセットしているため NSColor のシステムカラーは描画相手のことを知らないのに自身の値を変化させることができているようだ。

→ 続きを読む


  1. あのイベントで一人だけ涼しい服装をしていたのは自分が季節感のない人間だからではないと強調しておく。 ↩︎

  2. 個人的にはアクセントカラーが選べるようになったことの方がうれしい。 ↩︎

“Mac 開発話が聴ける Podcast エピソードまとめ”を公開

MacDev

iOSDC に初参加した勢いで勝手に変なまとめを作ってしまった。

iOS の世界とは異なり、macOS のソフトウェアを開発する人は少ない。さらに Cocoa ネイティブ、国内に限定したら 100 人いるのか不安になるレベル。

そんな開発者がごくまれに Podcast や YouTube に登場して話をしていることがある。どれも滅多に聞けない貴重な内容であり、本当に必要としている人に届かないのはもったいない。

そこで、このページではそんなエピソードを発見次第登録していく。

音声メディアじゃないと発信されないような内容も多いので、少しでも興味がある人は聞かないともったいない!

RawRepresentable のままで Dictionary にアクセス

MacDev
enum MyKey: String
{
    case identifier
    case colorCode
}

func process(_ dictionary: [String: Any])
{
    let identifier = dictionary[MyKey.identifier.rawValue] as? String
    let colorCode = dictionary[MyKey.colorCode.rawValue] as? Int
    ...
}

この .rawValue を何度も書きたくなかったので extension を書いたという話。シンプルなコードなので誰が書いてもほぼ同じになりそう:

extension Dictionary
{
    subscript<WrappedKey: RawRepresentable>(_ key: WrappedKey) -> Value? where WrappedKey.RawValue == Key
    {
        get { return self[key.rawValue] }
        set { self[key.rawValue] = newValue }
    }
}
これがあると
let identifier = dictionary[MyKey.identifier] as? String
let colorCode = dictionary[MyKey.colorCode] as? Int
こうなる

もちろん最初から [MyKey: Any] にした方がきれいな状況であればその方がいいと思う。すっきりした勢いだけで書いた内容のない記事でした。

© 2005-2018 zumuya