記事一覧に戻る

Macに乗り換えたので、まず画面の隅にイルカを住まわせた

SwiftSwiftUImacOS生成AI個人開発

MacBookが届いた日に思い出した、あのイルカ

2026年6月、長年使ってきたWindowsからMacBookに乗り換えました。開発環境としてのMacは快適です。それでも乗り換え直後の私は、細かい操作の違いに毎日ひっかかっていました。スクリーンショットの撮り方、ウィンドウの操作、ファイル名の全角問題。「今この場で、Macの操作をさっと教えてくれる存在」が欲しい。

そこで思い出したのが、かつてWindowsのオフィスソフトに住んでいたイルカです。正直、役に立った記憶はほとんどありません。それでも「画面の隅に誰かがいる」あの感覚だけは、妙に覚えていました。どうせなら乗り換え記念に、あのイルカをMacに住まわせよう。業務ツールより先に、私はこれを作ることにしました。MacKairu(マッカイル)の始まりです。

コンセプトは「ちょっと賢くて、ちょっと邪魔な常駐マスコット」。クリックすればAIコンシェルジュとしてMacの操作を教えてくれる。放っておくと画面を勝手に泳ぎ回る。役に立つことと邪魔なこと、両方をまじめに実装します。

技術選定: Electronを使わず、依存ゼロのネイティブで

常駐マスコットは一日中画面に居座るアプリです。だからこそ軽さを最優先し、ElectronではなくSwiftUI + AppKitのネイティブ実装を選びました。外部パッケージへの依存はゼロ、Swift Package Managerの標準構成だけでビルドできます。本体はSwiftファイル24本・約3,900行に収まっています。

キャラクターは画像アセットを使わず、SwiftUIのCanvasコードで直接ベクター描画しています。イルカ・ねこ・ペンギン・ひよこの4種をパスの組み合わせで描き分け、拡大しても輪郭が崩れません。アニメーションフレームを持たないので、この4種については画像アセットの管理が存在しません。動きをどう作っているかは、後述の「ゲーム開発の考え方」で書きます。

イルカねこペンギンひよこ

この4匹は、全員コードで描かれています

ウィンドウ設計: フォーカスを奪わない透明パネル

常駐マスコットの技術的な核心は、実はウィンドウ管理です。要件を分解するとこうなります。

  • 常に最前面にいる。ただし作業のフォーカスは奪わない
  • 背景は完全に透明で、キャラクターの形だけが浮いている
  • どの仮想デスクトップ(Space)に切り替えてもついてくる
  • Dockには現れない(アプリ切り替えの邪魔をしない)

これらはNSPanelのサブクラスに、borderless + nonactivatingPanelのスタイル、floatingのウィンドウレベル、透明背景、全Space追従のcollectionBehaviorを組み合わせて実現しました。borderlessなパネルは通常キーボード入力を受け取れないため、canBecomeKeyを上書きしてチャット入力を可能にしています。Dock非表示はLSUIElementとアクセサリのアクティベーションポリシーで行い、操作の入口はメニューバー常駐アイコンに置きました。

ドラッグで移動、ピンチで0.6倍から10倍まで拡大でき、位置と倍率は永続化されます。拡大時のウィンドウ成長は右下を基準に固定し、画面からはみ出す場合は自動で画面内に収める——という地味な調整が、常駐アプリの「気にならなさ」を支えています。

AIコンシェルジュ: 3社のAPIを直叩きする

クリックするとチャット欄が開き、Macの操作をAIに質問できます。システムプロンプトで「Windowsからの乗り換えユーザーには、Windowsでの同等操作を添えて説明する」という人格を固定しており、乗り換え直後の自分がそのまま最初のユーザーでした。

実装面の判断は2つあります。

1. SDKを使わず、各社のREST APIを直接叩く。 Claude・OpenAI・Geminiの3プロバイダを設定で切り替えられますが、外部SDKは入れていません。リクエスト生成と応答解析はRequestBuilderというネットワークに依存しない純粋関数に集約し、実通信は薄いURLSessionラッパに分離しました。この分離のおかげで、3社ぶんのリクエスト整形(マルチモーダルの画像の詰め方を含む)がそのままユニットテストの対象になります。

2. APIキーをリポジトリにもアプリバンドルにも置かない。 キーは初回設定時にホームディレクトリ配下の設定ファイルへパーミッション0600で保存し、公開リポジトリには保管コードだけが載っています。設定ファイル本体を書き出すときも、キーだけを空にしてから書く実装にしています。

クリップボードの画像やスクリーンショット(screencaptureをプロセス起動し、撮影中は自分のパネルを隠す)をそのまま質問に添付できるので、「この画面のこれは何?」という乗り換えユーザーの質問がいちばん短い動線で成立します。

遊びを仕様として実装する

このプロジェクトの半分は、まじめな技術で不まじめなものを作る遊びです。ただし私は、遊びも仕様として決めてから実装しました。

  • 勝手に泳ぐ: 放置すると7〜16秒間隔で画面内を移動します。2割の確率でマウスカーソルのそばに寄ってくる、という「邪魔さの調味料」も入れました
  • 話しかけてくる: 30〜70秒間隔で、Mac操作のTipsや軽口を吹き出しに出します
  • 太る: チャット履歴が増えるとキャラクターが少しずつ太り、履歴をクリアするとスリムに戻ります。会話量という見えない状態を、体型という見える状態に写像しています

スリムなカイル話し込んだあとのカイル

左が通常。右が、話し込んだあと。履歴を消すと痩せます

  • 簡単には消えない: 終了しようとすると引き止めダイアログが出ます。しかも「消す」と「やめる」のボタン位置とEnterキーの既定を毎回ランダムに入れ替えるため、連打では消せません。さらにlaunchdのユーザーエージェントが15分ごとに復活を試みます(この挙動は設定でオフにできますし、アンインストール手順も公開しています)

かつてのイルカが愛されつつ疎まれた理由を、現代のmacOSで再現するとどうなるか——という実験でもあります。

チャットに「お前を消す方法」と入力すると、調べた上でさようならと言って消える

ちなみに、例の「お前を消す方法」をチャットに打つと、彼は調べたうえで本当に消えます。……15分後、何事もなかったように戻ってきますが。

もうひとりの住人がいる

実はこのアプリには、イルカたちのほかに、もうひとりの住人がいます。ある言葉を唱えると顔を出す、カーソルに甘える女の子——POIN(ポイン)です。

POIN — カーソルに甘える、ウザいけど憎めないデスクトップ常駐キャラクター

呼び出し方は、リポジトリのどこかに書いてあります。そして白状すると、このアプリでいちばん実装が濃いのは彼女です。

イルカがベクターなら、彼女は一枚絵。 4匹の動物がコード描画なのに対し、POINは透過PNG 28枚の描き分けで表情と仕草を切り替えます。挨拶する、カーソルを追いかける、撫でられて目を閉じる、抱えられる、振り回されて目を回す——それぞれに絵があります。

「待ってください、そっちですか?」とカーソルを追いかけるPOIN撫でられて目を閉じるPOIN

カーソルを追いかけ、頭を撫でると目を閉じる

撫で心地は、状態機械で作る。 頭を撫でる・つかむ・振り回す——マウスとの触れ合いは約300行の純粋な状態機械に集約し、231行のユニットテストで固定しています。撫でられる「頭ゾーン」の当たり判定はウィンドウ枠の割合ではなくキャラクターの実寸から導出していて、ピンチで拡大しても撫でられる場所がずれません。

気分は、AIの返答で駆動する。 POINとの会話では、AIの返答末尾に気分タグ(傷ついた/大丈夫)をプロンプトで強制的に付けさせ、アプリがそのタグを読み取って表情に反映し、本文からは取り除きます。人格と表示状態を、LLMの出力そのもので連動させる設計です。心無い言葉への反応は二段構えで、明白な言葉はキーワード判定で即座に、微妙な冷たさはPOIN自身の返答タグで拾います。

ちなみに彼女は、どれだけ話し込んでも太りません。次の節の話は、半分は彼女のためのものです。

キャラクターが「生きている」ように見せる — ゲーム開発の考え方を借りる

かわいさの正体は、止まらないことだと思っています。MacKairuのキャラクターは待機中も常にわずかに上下し、4秒に一度まばたきし、泳ぐときは体をくねらせます。この「生きている感」の実装は、ゲーム開発の定石を借りました。

描画は毎フレーム、状態から合成する。 SwiftUIのTimelineView(.animation)が毎フレーム時刻を配り、描画側は「泳いでいる」「考え込んでいる」「挨拶している」「目を回している」といった現在の状態と時刻から、上下の揺れ・まばたき・体のくねり・傾きを三角関数の合成で導きます。用意したアニメーションを再生するのではなく、状態を毎フレーム描画する——ゲームループの考え方です。

アプリ自身のステータスを、表現に写像する。 チャット履歴が増えると太るのは、履歴件数から体型への写像です。さらにOSのメモリ圧迫通知と自プロセスのメモリ使用量を監視していて、負荷が高まるとキャラクターの振る舞いに「重さ」が現れます。マシンのステータス取得すら、かわいさの材料にできます。

振る舞いの切り替えは、ステートマシンと優先度で管理する。 考え込む・振り回されて目を回す・過負荷——表示が競合しうる振る舞いは状態として列挙し、いまどれを出すかは優先度チェーンで解決します。いちばん複雑な触れ合い演出(POINの撫で・保持まわり)は約300行の純粋な状態機械として切り出し、231行のユニットテストで固定しました。ゲーム開発でいうステート管理をUIアプリに持ち込むと、演出をいくら足しても壊れない骨格になります。

振り回されて目を回すPOIN

振り回すと目を回す——これも状態機械のひとつの状態です

構造: 純粋ロジックとUIの分離

開発の後半で、UI層に肥大化していたロジックをKairuCoreという純粋ロジック層へ切り出すリファクタリングを行いました。プロバイダ定義・リクエスト生成・キャラクターの状態機械・負荷判定などを時計にもネットワークにも依存しない形でcoreに置き、700行超のユニットテストで固定しています。

常駐アプリは再起動して目視確認するコストが高いので、「目視でしか確認できない範囲」を狭める構造が効きます。

Macが届いてからの10日間

最初のコミットは2026年6月5日、Macが届いた直後です。そこから6月15日までの10日間・53コミットで、コア機能から品質フェーズ(IME入力の修正、神オブジェクト化していたモデルの分割、コアロジックの抽出とテスト追加)まで到達しました。実装はClaude Codeとの対話で進め、ウィンドウ座標系のような「読んだだけでは分からない」領域はデバッグオーバーレイを作って実測しながら詰めています。

MacKairuはMITライセンスでGitHubに公開しています。macOS 14以降で動作し、ビルドは./build.sh一発です。Macの隅に、少し賢くて少し邪魔な住人はいかがでしょうか。

学び

  • 常駐アプリの本体はウィンドウ管理。キャラクターやAIより先に、「フォーカスを奪わない・透明・全Space追従」の土台が体験を決めます
  • 純粋関数への分離はネイティブアプリでも効く。API整形や状態機械をUIから切り離した時点で、テストできる範囲が一気に広がりました
  • 遊びは仕様化すると強い。「2割でカーソルに寄る」「ボタンをランダムに入れ替える」のような数値まで決めて実装すると、冗談が体験として成立します