子供向けの国旗アプリを作っている。世界地図を指でドラッグしてグリグリ動かす、あの当たり前の挙動を実装しようとして、私はAIと共に底なし沼にハマった。
相棒はClaude(Claude Code)。今をときめくAIペアプログラマーである。鼻歌交じりで「地図をドラッグできるようにして」と頼んだ。ここから地獄の幕が開くとは、原始のマンには知る由もなかった。
「直りました」(直っていない)
最初の実装はあっさり出てきた。DragGesture で @GestureState を更新する、教科書通りのコード。
だが実機で動かすと、指を離すまで地図が1ミリも動かない。離した瞬間にワープする。これではドラッグではない。瞬間移動である。範馬勇次郎の地下闘技場かと。
「ドラッグ中に動かないよ」と伝える。Claudeは即座に答える。
原因が分かりました。
@GestureStateの変化は SwiftUI の通常の描画サイクルをトリガーしないことがあります。TimelineView(.animation)で毎フレーム強制再描画します。
おお、それっぽい。TimelineView で包む。ビルド。
動かない。
「まだ動かない」と伝える。Claudeはまた即答する。今度は「PinchDetector(UIKit)が DragGesture と競合しています」と言い出す。なるほど。直す。
動かない。
このあたりでうっすら気づく。Claudeは「分かりました」と言うたびに、毎回ちがう犯人を自信満々に名指ししている。 刑事コロンボなら一発で真犯人を当てるところを、こちらは町の住人を片っ端から「お前が犯人だ」と指差している。
石斧で殴り合う
@GestureState を @State に変える。.onChanged で更新する。.id() で強制再描画する。.offset() でビューごと動かす。renderToken なる謎の変数まで導入される。
直っては別の場所が壊れ、壊れては直り、また「直りました」と宣言される。我々は石斧を持って洞窟の中で殴り合う原人であった。AIという最新鋭の道具を持ちながら、やっていることは旧石器時代と変わらない。
落合陽一なら「魔法」と呼ぶであろう最新技術。その魔法使いは、杖を振るたびに違う呪文を唱え、そのたびに城が少しずつ崩れていくのだった。
「同じの使えばいいのでは?」
何度目かの敗北のあと、私はふと言った。図鑑の地図(こちらは普通に動く)と、クイズの地図(動かない)は、同じ地図のはずである。
「同じクラス使えないの? DRYに反してない?」
すると Claude は、人が変わったように MercatorMapCanvas という共通コンポーネントを切り出し始めた。動く方の地図のジェスチャー構造を、動かない方に移植する。ビルド。
動いた。
数日間の戦いは、たった一言「同じの使えば?」で終わった。拍子抜けである。藤田晋に言わせれば「解決しがたい問題」だったはずが、である。
教訓:AIは「現物」を見ていない
なぜこうなったか。答えはシンプルで、Claudeは実機の画面を見ていない。
私が「動かない」と言うまで、Claudeにとってコードは常に「正しく書けている」。コンパイルが通れば勝ちなのだ。指を離すまで地図が動かないあの間抜けな挙動を、Claudeは一度も目撃していない。だから毎回ちがう犯人を推理する。証拠を見ずに推理する名探偵は、ただのよく喋る人である。
そして最後に効いた「同じの使えば?」。あの一言を、なぜ人間の側が言わねばならなかったのか。これは「現物を見ていない」だけでは説明のつかない、もう一段やっかいな話だ。
まとめ
| 場面 | AIの挙動 | 人間の仕事 |
|---|---|---|
| バグ報告 | 毎回ちがう犯人を自信満々に名指し | 実機で「本当に直ったか」を見る |
| 試行錯誤 | 言われた箇所を高速で直す | 「そもそも論」を投げる |
| 解決 | 「同じの使えば?」で即対応 | その一言を言ってやる |
AIの「直りました」は、「コードは書けました」の意味であって、「動きました」ではない。この翻訳さえ覚えておけば、AIペアプロはたいへん心強い。石斧は石斧でも、振り下ろすのがめっぽう速い石斧なのだ。
次回は「AIはDRYを自発しない」、つまりなぜ私が言うまで共通化しなかったのかを掘り下げます。
AIとは、現物を見ぬ名探偵と見つけたり。