派生型プロパティを Decodable で扱う

macOS 開発

最近の Swift では Decodable に準拠したオブジェクトを簡単に JSON から変換できる。

struct Root: Decodable
{
    let texts: [String]
    let isFine: Bool
}

let root = try JSONDecoder().decode(Root.self, from: """
{
    "texts": ["hello", "again"],
    "isFine": true
}
""".data(using: .utf8)!)

root.texts //["hello", "again"]
root.isFine //true

上のようにあらかじめかっちりと定義した通りのデータが与えられるときは自動でやってくれて便利だけど、実際にはそうもいかなくて難しい。

例えば、次のようにバリエーションが複数ある場合:

protocol Parameter: Decodable
{
    var type: ParameterType { get }
}
struct BoolParameter: Parameter
{
    let type: ParameterType
    let defaultValue: Bool
}
struct NumberParameter: Parameter
{
    let type: ParameterType
    let defaultValue: CGFloat
    let minValue: CGFloat
    let maxValue: CGFloat
}
enum ParameterType: String, Decodable
{
    case bool
    case number
}

BoolParameter と NumberParameter は同列な存在でどちらも Parameter に準拠させている。JSON では独自の型情報を定義できないので Parameter.type を用意して区別することにする。

続きを読む

JXA で Promise

macOS 開発

Yosemite から AppleScript の代わりに使える JXA、せっかく JavaScript なのだから Promise が使えるかどうか実験してみよう。

const getWindowNames = async (appName) => {
    const app = new Application(appName);
    const windows = app.windows;
    return windows.name();
}
const getFileNames = async (path) => {
    const fileManager = $.NSFileManager.defaultManager;
    let error = $();
    const fileUrls = fileManager.contentsOfDirectoryAtURLIncludingPropertiesForKeysOptionsError($.NSURL.fileURLWithPath(path), [], 0, error);
    return fileUrls.js.map(url => url.lastPathComponent.js);
}

let x = null;
Promise.all([
    getFileNames("/System"),
    getWindowNames("Finder"),
    getWindowNames("Calendar")
]).then((result, error) => {
    x = result;
});

const runLoop = $.NSRunLoop.currentRunLoop;
while (x === null) {
    runLoop.runModeBeforeDate($.NSRunLoopCommonModes, $.NSDate.dateWithTimeIntervalSinceNow(0.001))
    console.log("a")
}
x
Code

Promise の完了を待つように Run Loop を回しているのがポイント。これがないとすぐに終了してしまうし、ただのループだとずっとぐるぐる、何も起きない。スクリプトエディタ内で気軽に動かしたいだけなので idle ハンドラは試さず。 

関数を定義するときに async も使ってみたけど、どうだろう。

続きを読む

CGEventTap のタイムアウト

macOS 開発

先ほど Frog's Hand 1.0.1 のリリース記事(→ 2019.5.31)を書いた。...短いですね。今月の記事ノルマのためだけに内容のない記事を書くのもあれなので技術的な視点から少しだけ補足説明。

macOS では1CGEventTap という API2 でシステムに発生したイベントを横取り(フック)して別のイベントに置き換える3ことができるのだけど、そういうことをしているアプリケーションが何らかの原因で反応しなくなるとイベントをその先に送信できなくなり、システムがフリーズしたように操作を受け付けなくなってしまう。

そこでその手の API には安全機能がある。決められた時間内に処理が完了しなかった場合、システムはそのアプリケーションにフックさせるのを停止するのだ。

Frog's Hand も CGEventTap を使ってドラッグのマウスドラッグのイベントに対して処理をしているのだけど、普段は問題なく動作していても Mac がスリープしたりすると処理できないのか効果が切れてしまうことがあった。

その対策として定期的に発動するタイマーで CGEventTap が無効になっていないかを確認し、必要であれば有効にする処理を追加したのである。これで解決していればよいのだけれどいかがでしょうか。


  1. ちなみに Windows だったら SetWindowsHookEx() がそれっぽい。 ↩︎

  2. Swift では CGEvent のクラスメソッドっぽく使えるようになってる。CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) などがそれ。 ↩︎

  3. 置き換える必要がなければ NSEvent.addGlobalMonitorForEvents(matching:handler:) も使えそう。 ↩︎

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

macOS 開発

年末に反省したのに(→ 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] に非対応 

macOS 開発

-[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

macOS 開発

昔はよく使ってた 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 側のバグだったりするのが泣ける。 ↩︎

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

macOS 開発

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

続きを読む


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

© 2005-2020 zumuya