タグ: Tips and Tricks

  • Notes:iCab スイッチャーがつまづきそうなところ

    昨日は、フィードバックまつり と題して(勝手に)、昨年から「フィードバックしなきゃなぁ…」と気にしていた Mac アプリ開発者の方々に、あらかたフィードバックやリクエストを送りました。と言っても、メール4通 + フォーラムへの投稿1本を、ちょこちょこっと英語で書いただけなのですが、疲れたし、時間もかかった。もっとサクサクできるようになりたいものです(涙)。

    今日は、前回の記事 であれだけ、今からメインブラウザにしても問題ないレベル などと煽っておきながら、実際に使い続けてみると、現在のメジャーなブラウザに慣れている方がつまづきそうなポイントが2つほどあったので、ご紹介しておきます。ちなみに、いずれも iCab 公式サイトの FAQ に答えが書いてありました。

    Google のウェブサービスを正常に使うことができない

    (※ これは Leopard をお使いの場合の対応の仕方です。Tiger 以前をお使いの場合は、方法が異なってくるかもしれません)

    例えば iCab で Gmail にアクセスすると、「完全にサポートされたブラウザ」でない、として、「簡易 HTML 形式のページ」に飛ばされてしまいます。

    これは Google が、「サポートされたブラウザ」を、機能ではなく名前で判断しているからで、Google に対応ブラウザとして登録されていない iCab は、実際には動作可能(WebKit を使っているから、Safari と同等)であっても、対応ブラウザとして認識されないようです。

    iCab には、UA名をオン・ザ・フライで変更できる機能( View > Browser Identity )が備わっていますので、FAQ でも、ここで “Safari 3” に変更するように、と書かれています。

    が、少なくとも僕の環境では、上記のメニューに “Safari 3” という項目は見当たりません。とりあえず見つけた “Safari, MacOS X” を選択して試してみると、「通常の HTML 形式のページ」にアクセスすることはできたものの、JavaScript が正常に動作していないようでした。

    結論としては、自分で Safari 3 の UA名を設定 して、それを使います。やり方は:

    1. 環境設定パネル > Identity で、いちばん下のラジオボタンをオンにする
    2. ラジオボタンのとなりのフィールドに、Safari 3 の UA 名※ を入力する

      ※ 例:Mozilla/5.0 (Macintosh; U; Intel Mac OS X; ja-jp) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6

    これで、正常に使うことができるようになります。

    自動でパスワードを保存/入力してくれない

    設定すれば、自動でパスワードを入力させることはできます。しかし、いかなるフォームデータも、自動で保存されることはない ようです。FAQ によれば、これはセキュリティを(激しく)考慮した、仕様のようです。

    フォームデータを保存するには、フォームに入力した後、View > Save Forms を実行します。

    保存したフォームデータは、Tools > Forms Manager > Special Web Sites タブ で確認することができます(この他のタブがどういう場合に使われるのかは、まだ検証中です)。

    また、同タブにある Automatically fill out forms when entering a web page をチェックしておくと、他のブラウザと同様に、次に同じページにアクセスしたときに、保存したデータを自動で入力させることができます。

    ただし、ここがとても重要なのですが、このままでは、フォームデータを保存したときの URL と一字一句同じでなければ、自動入力してくれません 。これのなにが問題かというと、例えばログインフォームによくある、URL に sessionid が含まれていて、URL が随時変化する、といったものに関しては、自動入力がなされません。

    これを改善するには、保存したフォームデータを手で編集します。フォームデータ一覧の項目をダブルクリックすると、編集シートが現れますので、ここで必要な項目を編集します。

    項目の設定には、以下のワイルドカードを使うことができます:

    *
    任意の文字列を表す
    ?
    任意の1文字を表す

    FeedBurner のログインフォームを例にとると、図のようすれば、うまく自動入力されるようになります(sessionid を任意の文字列としているところが、まだちょっと甘いかもしれませんが)。

    まとめ

    …というわけで、気軽に乗り換えたい人にとっては、ちょっとハードルが高そう、ということが判明しましたが、僕は期待を込めて、まだまだ試していこうと思います。

    UPDATE: 最近、通常記事と Notes コーナー(カテゴリー)をちゃんと分けていないので、この記事は Notes コーナーに移動することに。Notes コーナーでは、僕の意見や感想よりも、各ジャンル(子カテゴリー)関して、すぐ参考にしていただける情報(問題の解決策か、問題について僕が調査できたところまで)を載せることを目指しています(いくつか最新のデータに更新できていないものもありますが…汗)。旧 URL へのアクセスは、リダイレクトされます。

  • AppleScript Studio から Cocoa API を利用する

    久々に AppleScript Studio の話題を。先日、applescript-studio ML に、

    • call method コマンドで Cocoa API を呼び出せるのはいいが、膨大な API の中には、AppleScript がネイティブに対応しているものもある。本当に必要な API がどれかを判断するには、どうしたらいいのか

    • また、その必要な API を AppleScript でラップして、AppleScript の知識だけあれば使えるような、ライブラリのようなものを作ったらどうか

    という趣旨(たぶん)の 投稿 がありました。

    で、僕はそれに返答を試みたのですが、まず英語に関して、読解力は、なんとかキープできていると思いますが、記述力は、もともと無いうえに、さらに悪化、高校レベルの基本的な文法を忘れている気がします。来年の目標が一つ確定(涙) 結局、自分が思ったことを、自分の記述力で漉してみたら、ちょっとしか汁が出なかった(?)という状態だったので、(日本語なら大丈夫かは別として)ここでリベンジしてみようかと。

    どうやって必要な API を見分けるか

    まず、最初の「どうやって必要な API を見分ければいいのか」ということですが、これは、見分ける方法を探そうとするよりも、素直に Cocoa/Objective-C を勉強して、それと AppleScript Studio がどのように関連しているのか、学ぶべきだと思います。

    「特殊な型の引数をとるメソッドは呼び出せない」とか「通知を受け取れない」とか「それをするにはどうしてもサブクラス化が必要」とか、ひとつひとつ挙げていくこともできるかもしれませんが、前述のような知識があれば、自然に分かることだと思います。

    例えば、Cocoa で NSTableView を使って表を表示させる方法が分かっていれば、それが AppleScript Studio 上のクラスやイベントハンドラとどう対応していて、call method コマンドを使えば、どういった追加の利益が得られるかは、だいたい分かると思います。

    そもそも、そのアプリは AppleScript Studio ベースでないといけないのでしょうか。Leopard で搭載されたスクリプティングブリッジを始めとして、いまは多くの選択肢がありますから、僕のように趣味でやっているのでない限り、その辺はシビアに考えるべきかもしれません。もし AppleScript Studio ベースで、しかもその枠を超えたことをしたいなら、普通に Cocoa/Objective-C アプリを作るくらいの覚悟がいるかもしれません。

    ライブラリは必要か

    また「必要な API をラップして、ライブラリ化したらいいんじゃないか」ということですが、まず、いま Apple が、Cocoa のパワーを Studio にも取り入れようと、必死で頑張ってくれているはずです!(かなり疑わしい 笑)僕たちがそれをすると、作業が重複してしまいます。また、ライブラリのようなものが出来上がったとして、今のところ AppleScript には、そういったものをうまく使いこなす機能が備わっていません( #import みたいのが欲しいな)。むしろ、前述のような知識があれば、必要な部分だけをピンポイントでラップして、効率の良いスクリプトにできるかもしれません。

    サンプルプロジェクト

    ところで、「ラップする」と言えば、僕も今まさに Kaku などで、AppleScript Studio がサポートしていないオブジェクトの「ラッパー」らしきものを作って、利用しています。そこで、そうしたラッパーに関連したコードを含んだ、僕が Kaku などで使っている基礎的なコードを集めて、サンプルプロジェクトを作ってみました。

    ダウンロード

    CallMethodOrientedProject.zip (63KB)

    ライセンス

    修正 BSD

    また、call method コマンドは、少しなら問題ありませんが、使う箇所が多くなってくると、かなりスクリプトが汚くなってくる、という気が(個人的には)します。このサンプルプロジェクトは、そういった問題への対処の一例にもなっていると思います。もし同じような問題で困っている方がいらしたら、見てみてください(たぶん、いない)。

    サンプルプロジェクト — 解説

    call method 実行のための関数群

    このプロジェクトに含まれているコードの特徴としては、まず、call method を使うときは、直接 call method 文を書くのではなく、スクリプトのトップレベルにある 、

    • cmoo() — 引数なしのインスタンスメソッドを実行
    • cmoc() — 引数なしのクラスメソッドを実行

    といった関数群を使います。これには、タイプするのがめんどい、という理由のほかに、メソッドの返り値で nil が返ってきたとき対処するため、という理由もあります。例えば、

    set var to call method "objectForKey:" of dictionary with parameter "unknownKey"

    といった文で nil が返ってきたとすると、AppleScript には nil かどうか調べる手段がない、というか、var の中身を取り出そうとした瞬間にエラーが発生するので、厄介です。前述の関数では、これらを事前にチェックして、もし nil なら、代わりに missing value を返します。

    以下は、ちょっと混乱しそうですが、test という文字列を保持している NSString のインスタンス( stringNSString は相互に変換される)の、uppercaseString というメソッドを呼び出す例です:

    cmoo("uppercaseString", "test") -- "TEST"

    ラッパーオブジェクト

    ここからが「ラッパー」の話になりますが、プロジェクトを実際にご覧いただいた方は、スクリプトの多くの部分が、スクリプトオブジェクトの組み合わせで書かれていることにお気づきかと思います。ラッパーは、script Object を継承する script Wrapper として定義されています。次はこの、Wrapper オブジェクトを使ってみます:

    set wrappedString to wrapperWithObject("test") of SO_Wrapper() -- Wrapper の生成
    cm("uppercaseString") of wrappedString -- "TEST"

    ちょっと、AppleScript 標準のオブジェクトを扱うようなスタイルに近づいてきました。

    カスタムラッパーオブジェクト

    さらに Wrapper を継承して String というオブジェクトを定義してみます:

    on SO_String()
    
        script |String|
    
            property parent: SO_Wrapper()
    
            on stringWithString(givenString) -- 初期化メソッド
                continue wrapperWithObject(givenString)
            end stringWithString
    
            on uppercaseString()
                return cm("uppercaseString")
            end uppercaseString
    
        end script
    
    end SO_String()
    
    ----
    
    set theString to stringWithString("test") of SO_String()
    uppercaseString() of theString -- "TEST"

    最後の実行文は、完璧とは言えないものの、だいぶしっくりくるようになったのではないでしょうか。他の種類のオブジェクトのためのラッパーオブジェクトも、上図のようにシンプルに Wrapper を拡張して、必要な機能だけを持ったものを、簡単に作れます。僕はこれを使って、Kaku などで、NSArrayController や WebView をコントロールしています。

    サンプルプロジェクトは、NSArrayController で管理されている URL リストに URL を追加し、その URL を WebView で表示する、というデモになっていて、NSArrayController の参照を取得する部分以外は、すべて AppleScript で動かしています。よろしければ、試してみてください。

  • Leopard:インターネット共有とファイアウォール

    いま、ケーブルモデムに有線でつながれた iMac を母艦に、AirMac 経由の インターネット共有 で、旧マシン iBook G4 でもネットに接続できるよう設定してあるのですが、これが Leopard にしてから接続できなくなってしまいました。

    いろいろいじってみた結果2 、これは根本的な解決策ではない気がしますが、僕の環境では、ファイアウォール の設定を 「必須サービスのみ許可」以外 にすると、とりあえず接続できるようになることを確認しました。「必須サービスのみ許可」では、インターネット共有による接続をブロックしてしまうようです。

    僕は、先日 CNET のこの記事 の「Appleのファイアウォールは、デフォルトではオンになっておらず…」の部分だけに反応して、慌ててオンにしたクチ(危ねえな)なのですが、この記事を改めて読んでみたり、その他いろいろ調べてみると、Leopard のファイアウォールには、いろいろ不安な部分があるようですね。

    そもそも、自分が許可しているインターネット共有サービスへのアクセスをがんがんブロックしたり、ぱっと見「必須サービスのみ許可」よりセキュリュティレベルが高そうな「特定のサービスおよびアプリケーションにアクセス設定」にすると余裕で接続できるなど、素人目にはよく分かりません。


    1. さらりと書きましたが、実際は、日がな一日見当違いのところをいじりまくって、果ては「アーカイブとインストール」で Leopard を再インストール。でもシステムが初期状態に戻ったので、爽快。 

  • Leopard:Spaces でひとつのアプリのウィンドウを、複数のスペースに配置して使う

    マルチウィンドウのアプリで Spaces を使ったら…?。どういう扱いになっているのか少し気になりますが、結論としては、もちろん、同じアプリのウィンドウでも、別々のスペースに配置して使うことができます。

    それよりも問題なのは、そのウィンドウを、どのスペースに配置したか覚えておかなければいけないのか、ということです(「それぐらい覚えろ」というご意見はごもっともですが)。実は、(完璧な解決策とは言えないものの)なかなかいい方法があります。

    Dock 上でそのアプリのアイコンをクリックすると、クリックするたび、順番に、そのアプリのウィンドウが含まれているスペースに切り替わります。これは、ノート型など、画面サイズがあまり大きくないマシンをお使いの方に、特におススメの機能です。

    どう使うかというと、例えば、Xcode のプロジェクトウィンドウとマニュアルウィンドウを、どちらも画面いっぱいにズームして、別々のスペースに配置してみたり…

    CSSEdit で、編集ウィンドウとプレビューウィンドウを、別々のスペースに配置してみたり…

    これは便利です。もう、スペースが何個あっても足りないというか。

    (※ あ、でも、ここまで書いといてアレですが、「Exposé と Spaces」環境設定パネル「アプリケーションの割り当て」 を行って、そのアプリをそのスペース内だけで使うようにすると、Exposé のスコープが狭まることになるので、より便利かも。…まあともかく、便利なんです。汗)

  • Leopard:Spaces で迅速に作業する

    複数のワークスペースを使って、画面をウィンドウで埋め尽くすことなく、それぞれのアプリでの作業に集中できる、Leopard の新機能「 Spaces 」。

    適当にウィンドウをスペースに割り振ってみたりして使ってみると、まあ確かに、以前より整然とした画面状態で作業することができるようになったかな、とは思いますが、その真価を知るには、もう少し、自分にとって使いやすい使い方を探ってみる必要がありそうです。

    ところで、ウィンドウをスペースに割り振るとき、いくらなんでも、毎回 F8 でスペースの一覧を出してから操作するのは、めんどいなあ。 と思っていたら、もっと直感的な方法も用意されているのですね。

    今日偶然気づいたのが、ウィンドウをドラッグして、画面の端(そのウィンドウを移動したいスペースがある方向)に持っていき、ちょっと待つ という方法。

    画面がスライドして、そのスペースに移動することができます。

    また、こちらほど直感的ではないものの、ウィンドウをクリック&ホールド(ドラッグしかけ状態)して、コントロールキー + アローキー/数字キーで希望のスペースに移動する という方法もあります。

    その他の操作方法については、Finder のヘルプメニュー「Spaces」を検索 すると出てくる、「Spaces で迅速に作業する」 が参考になります。

  • Notes:(調査中)Leopard AppleScript 日本語を使った XML-RPC が不安定に

    Leopard には、もちろん素晴らしいところが多々あるのですが、またイヤな情報をひとつ。Leopard 上で Kaku の動作をほとんど殺してくれている不具合について、少なくともどの辺にありそうかは分かってきました。

    「不安定に」と少し曖昧に書いていますが、僕が知る限り、2つの問題が組合わさっています。

    問題1. パラメータに日本語を含む XML-RPC を行うと、高い確率でクラッシュする

    スクリプトエディタから実行した場合はスクリプトエディタが、AppleScript Studio アプリから実行した場合は、そのアプリがクラッシュします。

    例えば、AppleScript Studio アプリから、日本語パラメータを含む XML-RPC を行うと、次のようなメッセージを残して、アプリがクラッシュします:

    XMLRPC Test(10908) malloc: *** error for object 0x2d4290: incorrect checksum for freed object - object was probably modified after being freed.
    *** set a breakpoint in malloc_error_break to debug

    パラメータに英数字を設定していれば、何の問題もありません。日本語の文字列の扱いでトラブっているでは、と想像できます。

    Leopard からは、スクリプト内に Unicode 文字を含めることが出来るようになりましたが、その辺との関連で、齟齬が起きているのかもしれません。

    問題2. パラメータに日本語を含む XML-RPC が成功したとしても、WordPress で文字化けする

    スクリプトエディタを使っている場合、問題1 が発生してスクリプトエディタが強制終了したあと、もう一度そのスクリプトファイルを開いて、すぐ実行すると、XML-RPC が成功する場合があります。

    しかし例えば、そうやって WordPress に記事を送信したとすると、記事の内容は「?」の羅列に化けてしまいます。

    問題1 が発生している時点ですでにあやしいですが、AEXMLTutor(Leopard でも動くことに感動)で送信している生の XML データを確認すると、次のようになっています(ちなみに記事の内容は「あああああ」):

    description あああああ

    文字参照になっているのが分かります。これが文字化けの原因だと思うのですが、この辺は(…というか、よく知っていること以外はだいたい)かなりうといので、よく分かりません。

    興味深いのは、これが Movable Type や P_BLOG × XML-RPC for P_BLOG (revision e) では、発生しないことです。これは、決して WordPress が悪いわけではなくて、たぶん、構図は:

    • 犯人
      • AppleScript
    • 犯行内容
      • 送信するデータがおかしい
    • 関係者
      • Moveble Type → なんとか対応
      • XML-RPC for P_BLOG (revision e) → なんとか対応
      • WordPress → 爆死

    …ということなんだろうと想像しますが、WordPress の実装は、他の2つとどう違うんでしょうか…?

    もしこれらに関して、新たに分かったことがあれば、ここに追記します。

    しかし、少なくともこれで、XML-RPC を AppleScript に任せたままで、手っ取り早く解決する方法はなさそうだ、ということは分かりました。なんとかせねば…

  • Notes: NSTextView へのドロップ

    この 記事 では、いろんなところでドラッグ&ドロップ操作ができるようにした、ということを書いていますが、いちばん対応に苦労したのが、NSTextView へのドロップ でした。

    NSTextView のサブクラスで、独自形式の項目のドロップを受け付ける、あるいは、ドロップされた項目の処理を、独自に実装するには

    障害になるのは、ドラッグ&ドロップを実装すること自体がちょっとややこしいことと、NSTextView が、標準でテキストやファイルのドロップに対応しているため、独自形式の項目をドロップできるようにするには、標準の実装を殺さないように、うまく立ち回らなくてはならないことです。

    これにはみんな苦労しているようで、cocoa-dev ML には、やたらとこのテーマの質問が投稿されていますが、その中で本当に役に立った記事を紹介しておきます:

    この記事では、 必要な registerForDraggedTypes: をしていないこと、 (標準で対応している項目の処理方法を変えるのが目的のコードだからか。) ドロップされた項目を、標準の実装に任せるのか、自分で処理するのか判定する部分の効率が悪いこと、-performDragOperation: の部分で、呼び出すスーパークラスのメソッドが間違っている、など注意しなければならない点がありますが、基本的には、これに習えばちゃんと動くはずです。(まあ、その前にちゃんと動かなかったのは、書き方が適当すぎたからですが 汗)

    カーソルの位置を、マウスポインタの位置に追従させるには

    テキストエディタで文字列をドラッグしてみると分かりますが、標準では、NSTextView 内のカーソルの位置が、マウスポインタの位置に追従します。しかし、ドラッグされている項目の処理を自分で受け取ると、これが動作しないようです。

    この場合、自分でカーソルの位置を再計算する必要がありますが、以下の記事に、そのためのコードが掲載されています:

    あとは -draggingUpdated: の中などで、自分(NSTextView)に setSelectedRange: してやれば OK です。ただし、別のウィンドウなどからドラッグしてきた項目の場合は、まだカーソルの位置が見えないので、-draggingEntered: の中などで、自分を firstResponder にしてもらえばいいでしょう。

  • あなたの Mac アプリをシェイプアップしよう — Vacuous Virtuoso

    更新頻度は高くないものの、かなりためになる 記事をアップしてくれる、Ankur Kothari 氏によるブログ Vacuous Virtuoso ですが、また面白い記事がアップされていました。

    Mac developer? Clean up your appVacuous Virtuoso

    Too many apps are shipped with debug symbols, uncompressed images, redundant files or generally useless rubbish that not only wastes users’ disk space, it ultimately ends up increasing the developer’s own bandwidth costs.

    多くのサードパーティ製(…とこの記事では書いてあるけど、Apple 製のものは完璧なんだろうか)Mac アプリでは、そのパッケージングに問題があり、非圧縮の画像リソースや必要のないファイルで、ユーザーのディスクスペースを無駄にしていることが少なくないそうです。

    それがどんなものかというと、記事で紹介されているものを挙げると、Finder 代替のファイルブラウザ Path Finder が、もとの 60MB から、簡単な処理で 30MB まで減らせる、といった具合。



    How to

    じゃあどうすればいいかというと、Ankur 氏が、4つにまとめてくれています。

    1. いらないものを消す
      • .DS_Store
      • Cocoa アプリなら、PkgInfo(Carbon アプリにしか関係ない)
      • nib ファイル内の:
        • classes.nib
        • info.nib
        • data.dependency
      • フレームワークを埋め込んでいるなら、その中のヘッダファイル
    2. ローカライズ用ファイル/フォルダを見直す
      • ローカライズに関係のないファイルは、各 .lproj フォルダに散在させず、Resources フォルダに置く(「関係ないファイル」というか、実際には同一なのに、手違いでローカライズ化してしまった Info.plist ファイルとか、僕はよくある 汗)
      • フレームワークを埋め込んでいるなら、自分のアプリがサポートしていない言語のローカライズ用ファイルまで入っていないか、ちゃんとチェックする。
    3. 画像リソースを圧縮する
      • TIFF 形式の場合には、特によく確認せよ、とのこと。
      • (簡単なグラフィックなら、NSBezierPath で描こうぜ!)
    4. バイナリをスリムにする
      • 「Debug」コンフィギュレーションでビルドしたアプリを公開しない(これは当然か)
      • デバッグシンボルを取り除く
      • できればユニバーサルバイナリだけでなく、各アーキテクチャ専用のバージョンも公開する。

    今回ご紹介している記事は、ウェブサイト Rixtep記事 を Ankur 氏が簡潔にまとめられたものなのですが、その記事では、さらに多くの項目が挙げられています。

    Rixtep の人は、どうもこの手のことに相当のこだわりがあるらしく、今回のようなパッケージングや、処理の効率性などに特に注目して(画面デザインや機能性についての話もありますが)、一部の Mac ソフトを完膚なきまでにこき下ろすレビューコーナー The Very Ugly は、作者さんの気持ちになるとちょっと涙が出ます。

    自動化

    いくらユーザーのディスクスペースを無駄にしないため、とは言っても、上記のような手順を、すべて手作業で行うのはとても面倒です。

    そこで Ankur 氏が、これらの作業の一部を自動化する trim-app というシェルスクリプトを公開してくださっています。ダウンロードは、記事 中ほどの「 Download trim-app 」から。ダウンロードしたら、chmod 0700 trim-app しておきましょう。

    trim-app の使い方

    詳しい使い方は、trim-app -h で確認することができますが、基本的な使い方だけ書いておくと、(処理は取り消しできませんので、ご注意を)

    % cd MyApp.app
    % trim-app
    Architecture to keep? (ppc/i386)

    どのアーキテクチャ用のバイナリを残すか聞かれますが、ここでそのまま return すると、Universal Binary のまま。アーキテクチャを指定すると、もう片方のアーキテクチャ用のバイナリが削除されます。

    trim-app を使うと何が起きる?

    これを実行すると、

    • パッケージ内の .DS_Store を削除
    • nib ファイル(パッケージ)内の不要ファイルを削除
    • 不必要なアーキテクチャ用のバイナリを削除(指定した場合)
    • デバッグシンボルを削除

    …が行われます。つまり、不必要なローカライズ用ファイル/フォルダ、フレームワークのヘッダファイルの削除などは、引き続き手作業で行う必要があります。

    Kaku で試してみる

    というわけで、さっそく Kaku(開発途上版)で試してみたのですが…

    Before

    After

    これに加えて、一応 PkgInfo ファイルやフレームワークのヘッダファイルなども削除してみましたが、う〜ん…、そもそもちょっと前までフロッピーに入るサイズだったし、画像リソースは PNG 形式だし、一部で使っているカスタムデザインのコントロールも NSBezierPath で描いてるし(このためにじゃないけど)、Kaku に関しては、あまり絞れるところはないようです。

    個人的な感想

    冒頭で挙げた Path Finder の 60MB → 30MB が本当なら、日本より回線速度の遅い海外の事情も考えると、かなりひどい話ですが、ハードディスク容量も回線速度も上がった昨今、そこまでストイックに容量を節約しなきゃダメ…? と思うのも正直なところです(使わないローカライズ用ファイルを削除したり、使わないアーキテクチャ用ののバイナリを削るユーティリティーがあるけど、まったく使おうと思ったことがない。ただ Performa 630 時代は、常に涙目)。

    ただ、そのアプリを使う側にとって、まったく関係のないファイルが大量に含まれている場合が(かなり)あるということを知り、驚きました。自分がアプリをリリースするときも(特にアプリの規模が大きくなってきたときには)注意しなければいけないな、と思いました。

  • Core Data で画像を扱う

    前回の記事 で、「 Core Data によって、プログラムの骨格を作るのはかなり楽になるけれど、それだけでちゃんとしたプログラムができるわけではないし、もしそれができなければ、いま作っている Kaku の画像挿入機能は搭載しない」 ということを書いたと思います。

    昨日はまさに、そういう「これじゃあ公開できない」という事態に直面していました。前回の記事を書く前に、うすうす気付いてはいたのですが、やはり大量の画像を登録したとき、画像挿入機能のパフォーマンスがかなり悪くなる のです。

    結局原因は、Core Data に頼りすぎた、とかではなく、設計そのものがおかしかったというか、単純に、もっと勉強してから臨むべきだった、ということでしたが…(汗)

    …というわけで今日は、その問題を解決していった過程を書いていきたいと思います。タイトルは「Core Data で画像を扱う」となっていますが、それについての 正しい答えが書いてあるわけではありません ので、悪しからず(僕が今のところどういった方法でやることにしたかは、書いてあります)。

    何しろ、プログラム開発の専門教育を受けたこともないし、もろ文系なので、あまり笑わずに(?)読んでください。何かもっといいアドバイスがありましたら、ありがたく頂戴いたします。

    Core Data で画像を扱う

    僕が画像を扱わなくてはならない理由は、画像挿入機能で Kaku に登録された画像のサムネイル です。

    本当に初期のテストでは、Core Data で画像ファイルのパスだけを保存しておいて、NSImageView の「 valuePath 」バインディングで表示させていましたし、オリジナルの画像データそのものを Core Data で保存してしまう、という方法も考えられますが、それだとどうしても パフォーマンスが悪くなる ので(…と言っても、最初の方法での動作がどんな感じだったかは、ちょっと忘れてしまった)、それならば、サムネイルを作って、一緒に保存しておこう、ということになります。

    そこで、そのために僕がとった方法を順に追っていきますが、僕が最初にとった方法は:

    • Cocoa で画像データを読み込むのって、どうやるんだっけ? [[NSImage alloc] initWithContentsOfFile:path] とかでいい?

    • Core Data では、直接 NSImage を扱うことはできないけど「バイナリ」なら扱える。

    • じゃあさっきの NSImage のインスタンスをアーカイブ化して保存しよう。

    …という、あまり深く考えていないものでした。

    方法1

    1. エンティティに、種類 = バイナリ の属性を追加する。

    2. このエンティティに基づいた管理対象オブジェクトを生成するとき、上の属性に、以下のようにして生成したサムネイルデータを設定する。

      • 画像ファイルから NSImage のインスタンスを生成。

        NSImage *thumbnail = [[NSImage alloc] initWithContentsOfFile:originalFilePath];

      • 縮小……?

        [thumbnail setScalesWhenResized:YES]; [thumbnail setSize:thumbnailSize];

      • アーカイブ化して属性値として設定。

        [managedImage setValue:[NSKeyedArchiver archivedDataWithRootObject:thumbnail] forKey:@"thumbnail"];

    3. 「 PPMKeyedUnarchiveFromData 」といった Value Transformer を作っておく。(ここではプリセットの「 NSUnarchiveFromData 」は使えないし、そもそも NSArchiver/Unarchiver は非推奨)

    4. 表示するときは、NSImageView などの「 value 」バインディング にバインドし、3. の Value Transformer をセットする。

    方法1 — 結果

    僕はこのサムネイルを NSTableView で表示するように設定したのですが… ハードディスクがすっごいガリガリ言ってる! スクロール重い!

    たぶんメモリ使用量はそんなに多くないと思いますが、どうもテーブルの新しい行が表示されるたびにアーカイブ化/非アーカイブ化が連発するようなので、ぜんぜん快適に表示できません。これは失敗でした。

    方法2

    これに懲りて、もうちょっとちゃんと調べてみることにしました。

    調べてみると、画像のように そのままでは Core Data で扱えないデータを扱う方法 を紹介したページとして、CocoaDev の「 BinaryInCoreData 」というページを発見しました。あとから調べてみると、アップルのドキュメントにも、これに関連した ページ が。

    方法2 はこれらのページを参考にしたもので、詳細はリンク先を読んでいただくとして、簡単に書くと:

    1. エンティティに以下の属性を追加:

      • 実行時用の 種類 = 未定義一時 = YES の属性

      • 保存用の 種類 = バイナリ の属性

    2. NSManagedObject のサブクラス を作り、それを上記エンティティのクラスとする。

    3. このサブクラスで、実行時用属性のアクセッサメソッド(など)をオーバーライドする。- (id)<実行時用属性名>- (void)set<実行時用属性名>:(id)newValue など )

      オーバーライドしたメソッドで何をさせるかというと:

      • 取得メソッドが呼ばれたら、実行時用の属性がすでに読み込まれているかをチェック( [self primitiveValueForKey:@"<実行時用属性名>"])して、まだ読み込まれていなければ、保存用の属性(バイナリデータ)を読み込んで、それを非アーカイブ化して、実行時用の属性として設定する。

      • 設定メソッドが呼ばれたら、それを実際に設定するほか、(必要なら)それをアーカイブ化して、保存用の属性に設定する。

      • (さっきから「など」を連発していますが)例えば - (void)awakeFromFetch をオーバーライドすれば、実行時用属性を読み込むタイミングを早めることができる(「まさにその実行時用属性を取得しようとしたとき」→「(ほぼ)プログラムが起動したとき」)。

    僕の説明ではよく分からないかもしれませんが、要は 方法1 とは異なり、ハードディスクからデータを読み込んで、非アーカイブ化するのが(ほぼ)1回 になります。ガリガリしません。

    「これは賢い!」 と思って早速この方法でやってみました。ところが…

    方法2 — 結果

    • 最初は パフォーマンス良好。
    • でも画像を 10 コ強登録していくと、だんだん動作が重くなってくる。
    • よく考えなくても分かることだけど、アクティビティモニタ.app で見たら メモリ使用量がスーパーインフレ。( 通常 20MB弱 → 100MB強
    • Kaku.xml ファイル(永続ストア)の容量は、だいたい 300KB の画像を登録すると 3MB 増えるペース(!!)

    方法1 も 方法2 も、扱うデータの種類が違えば、適用できる場面もあるに違いありません。しかし、画像データ、かつ数が不定、というこの場面では、ちょっとキツいものがあります。

    このあと、「じゃあサムネイルデータがいらなくなったら解放しよう」 と考えたのですが、これが罠でした。サムネイルがテーブルの枠外に出たときがベストタイミングですが、これを判定するのはかなり難しそう。じゃあメモリ上に読み込んでおけるサムネイルの数を制限する? これもちょっと難しそう。画面上で見えなくなったテーブルの行が判定できたとして、そもそもどうやって、その行のサムネイルデータを持っている管理対象オブジェクトを探し出すのか…。後から考えるとかなりアホらしいのですが、このときは、もはや開発続行不可能か と思われました。

    …ここまでが、前回の記事 を書いた後までの話。

    方法3

    結局どうやったら解決できるのか分からず、とりあえず Cocoa での画像の扱いや、どうやったら画像を早く処理できるのかを勉強し直してみることにしました。その中で、「 aaron evans » Blog Archive » CoreImage vs NSImage scaling 」という記事に行き当たったのですが、この記事を読んで、(本当に、かなり遅ればせながら)2つのことに気付きました。

    • これまでなんとなく NSImage をディスクに保存したり、読み出したりしていたけど、NSImage は高度な Cocoa オブジェクトであって、画像そのものを扱いたいならかなり無駄だし、いろいろコストがかかりすぎる。

    • (これは上ほど重要ではありませんが)てか、「方法1」の サムネイル生成の手法も、ちょっと適当過ぎかもしれない。(上の記事に書いてある方法が、画像の拡大縮小に関しての「 The classic way(=鉄板)」らしい)

    これを踏まえて作戦を考えると、

    1. エンティティに、種類 = バイナリ の属性を追加する。

    2. このエンティティに基づいた管理対象オブジェクトを生成するとき、上の属性に、前述の記事を参考にして元画像を縮小、かつ、ダメ押しで JPEG 圧縮して生成したサムネイル画像の バイナリデータ( NSImage のインスタンスではない )を設定する。

    3. 表示するときは、単に NSImageView などの「 data 」バインディングにバインドする。

    方法3 — 結果

    • 激速。あまりに速いので、楽しくてしばらくスクロールし続けて遊ぶ。
    • 試しに 画像を80コほど登録 してみたが、スピードは変わらず。
    • メモリ使用量は普段と変わらず。
    • Kaku.xml ファイルの容量は、ほぼ 画像数 × 10 = 800KB くらい。

    …というわけで、「『方法3』が見事!!」と言うより、僕の当初の発想が、見事なまでに間違っていた というべきですが、これで、画像挿入機能の搭載自体を取りやめる、という事態は避けられました。「 Core Data を使わなければいいのかな…」とも考えましたが、これでまたしばらくは、爽快な Core Data を使い続けていけそうです。

    本当は、前回のようにもう1セクション設けようと思っていたのですが、長くなりすぎたので(読んでくれる人いるのか?)、自粛。

    ※業務連絡:フィードを全文配信にしてみたつもりですが、成功しているか分かりません。この記事でチェック。

  • Core Data は素敵 / AppleScript Studio プロジェクトと Core Data

    最近、MarsEdit 2 を使って、それについての 記事 を書いてみたり、ecto 3 Alpha を使って 記事 を書いてみたりしていたわけですが、自分は?というと、Kaku の画像挿入機能を試しに作ってみていました。

    なぜいま画像挿入機能を作っているのかというと、他に作ってみたい機能や、他に作ってみたいソフトもあるのですが、現実問題、いまとりあえず足りないのは、Kaku の画像・リンク挿入機能あたりだろう、と。また、すでに書いたように、Mac 向けの二大ブログエディタを実戦で使ってみた結果、「やっぱり画像やリンクの扱いをなんとかしないと、記事を書くのがなかなかスムースにならない」ということを痛感したからでもあります。

    こういった機能を作るには、画像の扱いやら、外部ファイルの扱いやら、ドラッグ&ドロップの扱いやら、自分がこれまで学んできたことがほとんど役に立たなそうな領域(=避けてきた領域)に挑むことになるので、かなりビビっていますが(下にも弱気発言)、今日からしばらくは、その中で気付いたことを記事にしていきます(たぶん)。


    Core Data は素敵

    脱線(2)」で、Core Data ベースの Kaku「CoreKaku」を途中まで作ってみた、ということを書きましたが、このとき初めて実践した Core Data が楽しく、こういった機能を作るときは Core Data を使おう、と心に決めていました(というか、Core Data 以外でのやり方を考えるのが面倒くさいというか…)。というわけで、この画像挿入機能は、全面的に Core Data を使って作られています。

    まず練習用に、こんなプロトタイプを作ることにしました:

    • ドラッグ&ドロップで画像を登録して、管理するアプリ
    • データベースには、画像本体は置かず、画像ファイルのパスとサムネイルを保存
    • 画像のパラメータから img タグを自動生成

    しかしこれは、GUI 上の操作とコピペ(おい)で、2秒ほどで完成(ウソですが)。これ以上は、記事のデータと連携できないと意味がないので、早々と Kaku との統合に作業を移しました。Core Data 恐るべし。

    ss_image_manager.jpg

    (※ Core Data はこうして、中心となる機能が動き出すのはかなり早いのですが、かなり開放的、というか、本当にまともなプログラムとして仕上げていくのには、時間と知識が必要 な気がします。例えば、保存ファイルはデフォルトの1個のままでいいのか、とか、もし大量の画像が登録されても、そのままでパフォーマンスが出るか、とか。動くには動くんですが、それ以外の部分を僕の知識でカバーできるか心配。今は快調に動いているんですが、それができなければこの機能は載せないかもしれません。けっこう弱気。)

    AppleScript Studio プロジェクトと Core Data

    ここで、AppleScript Studio ベースである Kaku 上で Core Data が動くように しなければなりません。

    昔の僕なら「そんなことできるの?」と思っていたでしょうが、AppleScript Studio アプリも Cocoa アプリの変種(?)だ、ということを念頭に置けば、できないはずがありません。たぶん、非 Core Data の Cocoa アプリを Core Data 化するのと変わらないのではないでしょうか。

    必要な作業は:

    • Core Data.framework をプロジェクトに追加する。

    • 本来 Xcode で「ファイル」「新規プロジェクト」「Core Data Application」 とすると、始めから Core Data.framework を含んだプロジェクトとともに、データモデルファイル(「 AppName_DataModel.xcdatamodel 」)と、あらかじめ基本的なコードが書き込まれた、アプリケーションデリゲートのヘッダ/実装ファイル(「 AppName_AppDelegate.h/m 」)が入っている。

      これらは知識があれば自分で用意することもできると思うが、(とりあえずは)これらがあった方が楽なので、なんとかする。僕の場合は、別に「Kaku」という名の Core Data アプリケーションプロジェクトを作成して、そこから拝借。

    • あとは Objective-C/Cocoa + Core Data の知識があれば、call method でなんでもできるよ!(※素敵なパラドックス。そんな知識があるなら、なぜ初めからそれで作らないのか、と。笑 僕は AppleScript のほうが慣れているから、としか言いようがありません…ってそれもやっぱりおかしいような。)

    大体こんな感じでしょうか。

    逆にほとんど Objective-C/Cocoa で書いた Core Data Application の一部を AppleScript(Studio)で動かすことも可能。前述の CoreKaku がこのアプローチ。…ってこれ、よく考えるとちょっと便利かもしれないなぁ。Objective-C/Cocoa の部分と AppleScript(Studio)の部分は、僕の印象よりもはるかにシームレスに共存できるんだな、と感じました。

    ちなみに、こういった方法を最初に知ったのは applescript-studio ML のこの 投稿 です。できたら僕もまとめて、「Notes」コーナーに投稿しようと思います。

    ※ この記事は、かなり初期段階の画像挿入機能を備えた Kaku で作成しました。

    t_ss_kaku_with_image_manager.jpg