WKWebView 内の JavaScript から NSAlert を async await で呼び出す

MacDev

年末に反省したのに(→ 2018.12.29)またもや青いカテゴリーの記事だ1。とはいえ、いつもみたいに Cocoa ネイティブの話だけではない。

快適に“ひ to り go と”を更新するため自分専用に作っている記事エディタがある。記事やデータベースのファイルはすべてローカルにあるものを編集し、あとから手動でサーバにコピーするから 1 台の Mac 内で完結しているのだけど2、CMS のエンジンにアクセスする都合上エディタも PHP(とクライアント側の小さな JavaScript)で動いている…というのは以前書いた通りだ。

Web ブラウザ内での使用感では飽き足らずそれをラップする Cocoa App まで作ったものの、WKWebView の中では POST でフォーム上のデータを送信する仕組みのため、記事を保存するたびに画面が真っ白になって再読み込みがかかりエディタやプレビューのスクロール位置がリセットされてしまうものだった。保存せずに記事を離れたりウインドウを閉じるときでも警告すら出てくれない。

macOS 上で macOS らしくない体験はすべてノイズだ。純粋なネイティブ App ではないものの改善の余地はある。ちょうど別件で最近の JavaScript 事情を勉強していたところなので、ES2017 までの新機能をふんだんに使って記事エディタの中身を作り直したのである。

あらゆるものをウインドウの表示範囲の中で無理やり完結しているのが Electron みたいな Web ベースで作られたデスクトップ App の気持ち悪さの一因3だと思う。WKWebView にさせる仕事がほとんどだとはいえ、せっかく Mac 専用に App を作るのだ。積極的にウインドウの外にもはみ出していこう!

NSAlert を JavaScript から使う

これだけ長い前置きを書いたもののこの記事で触れるのは NSAlert だ。これをシートとして表示するだけでずいぶんと Mac App らしくなる。好きなボタンを並べられるし、“保存しない”ボタンを command + delete で押せるようにするようなカスタマイズも簡単だ。「保存しますか?」をネイティブなシートで表示させてみよう。

タイトルバーがないとシートを表示する場所に困る。

→ 続きを読む


  1. 実は昨年末に“買ってよかったものまとめ”を書いたりもしていたのだが内容を欲張りすぎて間に合わなかった... ↩︎

  2. 設計思想は完全にこの人の影響。 ↩︎

  3. 例えば環境設定をタブとして開く Web ブラウザなんて Mac では使いたくない。 ↩︎

Swift 4 の URL は -[NSURL fileReferenceURL] に非対応 

MacDev

-[NSURL fileReferencceURL] を使用すると AppleScript でいうところの alias みたいな1、ファイルの場所に依存しない特別な URL を取得できる。

NSURL* url = [NSURL fileURLWithPath: @"/Applications/"]; //1
url.fileReferenceURL //2
Code
1: file:///Applications/
2: file:///.file/id=3534367.8613694728/
Result

これを Swift で使うのが今回の話。

→ 続きを読む


  1. よく調べずに書いたが本当? ↩︎

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. 個人的にはアクセントカラーが選べるようになったことの方がうれしい。 ↩︎

© 2005-2019 zumuya