【iOSアプリ開発備忘録 #3】未経験者がぶつかった4つの実装地雷 — 物理・音・状態管理・アニメーション

Claude

React Native移行で開発は劇的にスムーズになった。 でも、そこから先で待っていたのは、未経験者だからこそ気付けない沢山の「実装の地雷」だった。

前回 では、Flutterを2日で諦めてReact Nativeに乗り換えた話をしました。そこからは順調に進んだ……と書いたものの、もちろん「全くノーストレスで完成した」わけではありません。

今回は、移行後の実装フェーズでぶつかった『4つの地雷』をまとめます。物理エンジン、音、状態管理、アニメーション―…どれも未経験者一人では絶対に気付けなかったポイントばかりでした。

すべて『AIと一緒に解いた』記録です。


第3回で扱う4つの地雷

最初に全体像をお見せします。

#地雷カテゴリ
1WebAudio合成の限界
2iOSのAudioContext制限音 / iOS固有
3音階設計の罠UX設計
4reanimatedを入れて削除した依存ライブラリ

「音」が2つあるの、SÉtelier(セトリエ:僕がapp storeにリリースしたアプリ)が音楽アプリだから当然なんですが、それを差し引いても『音まわりは未経験者には地雷だらけ』というのが率直な感想です。


地雷1:WebAudio合成の限界 — 鳥と猫を諦めた話

第2回で「React Nativeに移って、音源ファイルなしでWebAudio APIで全部合成できるようになった」と書きました。初リリース段階では、マリンバ・ベル・木琴・水滴・木魚・ガラス・ハンドパン・シンギングボウル・焚火・雨葉と、10種類のオトダマを音源ファイル0個で実装できています。

でも、最初は『もっと欲を出していた』んです。

実装してみたが諦めた音

開発初期、僕はオトダマのバリエーションをさらに広げたくて、こんな音も実装してみました。

  • 「鳥」のさえずり
  • 「猫」の鳴き声
  • 「氷」の音
  • 「水琴窟」の音
  • 雨だれの「ピチャン」という音

このうち、最終的に『生き残ったのはひとつもありません』。全部削除しました。

何が問題だったのか

WebAudio APIのオシレーターは、サイン波・三角波・矩形波・ノコギリ波という基本的な波形を生成するものです。これに周波数や振幅、エンベロープ(時間変化)を組み合わせて、楽器の音を「それっぽく」作っていきます。

マリンバやベルみたいな『打楽器・金属系の音』は、これでかなりリアルに作れます。立ち上がりが鋭くて、倍音構造もわりと単純だから。

でも、『鳥のさえずり』や『猫の鳴き声』はそうはいきません。

  • 鳥:周波数が複雑に変化する短いトリル、種類によって全然違う
  • 猫:声帯の共鳴、ビブラート、感情的な抑揚

これをオシレーターで作ると、どうしても『電子音っぽい不自然な何か』にしかならない。Claudeに何度もパラメータ調整を依頼したのですが、「鳥っぽいけど鳥じゃない音」「猫っぽいけど猫じゃない音」が出てくるばかりで。

諦めるという判断

ある日、ふと思いました。

「これ、前回Flutter諦めた時と同じ判断軸なんじゃないか?」

『Claudeでも自然な音が出せないなら、未経験者の僕が粘っても無理』。これに気づいて、5音種を一気に削除しました。

代わりに、『WebAudio APIが得意な音』だけに絞りました:打楽器系、金属系、ノイズベースの自然音(雨・焚火など)。

今回、音源ファイルを使わなかった理由

「実音源ファイルを使えばいいじゃん」という意見はもちろんあります。実際それはそれで一つの解です。

でも、SÉtelierの『音源ファイル0個でアプリが完成する』という設計思想は、容量の軽さやライセンス管理の手間という観点で大きなメリットがあります。だから僕は『WebAudioで自然に作れる音だけ』をルールにすることを選びました。

学び:『AIが詰まる音は、その技術スタックでは作れない音』 ──前回の判断軸が、別の文脈でも有効だった

ただ、より良いユーザー体験を追求するのであれば、音自体のクオリティアップや音の種類を増やす必要があり、それはコード生成だけでは限界があります。
開発に慣れてきた頃にまた、音声ファイルの仕様含め、別のプロセスの導入も検討し試行錯誤してみるつもりです。


地雷2:iOSのAudioContext制限 — 経験者なら見えていた落とし穴

僕の場合、症状が出る前にClaudeから事前に対策を提案されていたパターンでした。

実装を進めていたある時、Claudeからふと指摘がありました。

「いまSÉtelierでは、オトダマ用と環境音BGM用で別々にAudioContextを作っていますが、iOSではAudioContextの数に上限があるので、アプリ全体で1つに統一しておいたほうがいいですよ」

未経験者の僕には、この時点で何のことを言っているのかほぼ理解できませんでした。

  • AudioContextって何?
  • 数に上限?
  • いま動いているのに、なんで変える必要が?

正直なところ、『動いてるんだから、後でいいんじゃないかな』と一瞬思いました。
経験のない人間にとって、『今は問題ないけど後で問題になる』という話はピンとこないんです。

Claudeの説明:iOSの隠れた制約

それでもClaudeに理由を聞くと、こう説明されました。

WebAudio APIには『AudioContext』というオブジェクトがあって、音を鳴らすための入れ物のような存在です。これがiOS(特にSafariベースのWebView系)では『1アプリで作れる数に上限がある』という制約を持っています。具体的には4つ程度。さらに、不要になったAudioContextを明示的にcloseしないとメモリに残り続けてしまう。

つまり、放置すると後々こういう症状が出る可能性が高い:

  • アプリ使用中、ある瞬間から急に音が鳴らなくなる
  • アプリを再起動すれば戻るが、しばらくすると再発する
  • ユーザーは原因が分からないので不快な体験になる

これを聞いて、『動いている今のうちに直しておこう』と判断しました。

解決策:アプリ全体で1つに統一

採用したのは、Claudeから提案された設計です。

『アプリ全体でAudioContextを1つだけ作って、それをオトダマもBGMも全員で共有する』

src/audio/sharedContext.ts という共有モジュールを作って、最初に1回だけAudioContextを作る。あとは全員そこから取り出して使う。これでAudioContextの数は常に1つに固定されます。

結果として、SÉtelierではこの『AudioContextの数制限による音切れ』を一度も体験していません。事前に潰しておけたバグだったわけです。

「予防的なリファクタリング」を素直に受け入れた話

経験者なら『あ、AudioContextの数制限ね』と即座にピンとくる定番の制約だそうです。
でも未経験者の僕には:

  1. AudioContextというオブジェクトが何かを知らなかった
  2. iOS固有の制約があることも知らなかった
  3. 『数の上限』という概念自体が存在することを知らなかった

Claudeが先回りして教えてくれなかったら、リリース後にユーザーから『音が鳴らないんだけど』と報告が来て、初めて気付いて慌てて直す――そんな展開になっていたと可能性もあります。

未経験者にとって、AIの提案する『予防的な設計改善』は、その場では価値が分からないことが多いんですよね。『今動いてるのに、なんで変える必要が?』と感じる。でも、そういう時こそ素直に従っておいたほうがいい。これが『AIが先回りして教えてくれる知識』を信用するという、もうひとつの判断軸でした。

学び:プラットフォーム固有の制約は、未経験者には予測不能。 AIが『今のうちに直しておこう』と提案してきた時は、素直に従うのが吉。


地雷3:音階設計の罠 — 「自由度=良いUX」ではなかった

これは技術的な地雷というより『設計判断の地雷』ですが、SÉtelierの体験を決定づけた重要な意思決定なので入れておきます。

当初の設計:全音階(クロマチック)対応

最初、僕はオトダマの音の高さを『半音単位で自由に変えられる』ように設計しました。C3からC6までの3オクターブ、半音含めて37音すべて選べる。

『自由度が高い方がユーザーに優しいだろう』と思ったんです。これが完全に間違いでした。

何が問題だったか

実際にテストしてみると、こうなりました。

どう置いても、不協和音が混ざる』

ユーザーが何も考えずにオトダマを並べると、半音違いの隣り合った音が同時に鳴ったりして、『気持ち悪い響き』が頻繁に発生する。アンビエントBGMアプリなのに、リラックスできないどころか『なんか不快』と感じる音楽になってしまう。

これは、未経験者の僕には『何がいけないのか』すら分からない問題でした。「音楽理論」というものを学んだことがなかったから、『音には組み合わせの良し悪しがある』ことを知らなかったんです。

解決策:ペンタトニックスケール

ここでもClaudeに相談して、『ペンタトニックスケール』という解決策を教えてもらいました。

ペンタトニックスケールというのは、5つの音だけを使う音階(C, D, E, G, A)で、『どの音をどう組み合わせても不協和音にならない』という性質を持っています。日本の童謡や民謡、世界中の民族音楽、現代のポップスでも広く使われています。

これを採用すると、ユーザーがオトダマをどこに置いても、『心地よく響く音楽』しか生まれなくなるので、有意義な修正でした。

ちなみに、クロマチックは「全音階モード」と称して、有料のプレミアムプランで解放できるように課金要素として取り入れました笑

「自由度」と「優しさ」は別物だった

ここで僕は、UX設計の根本的な学びを得ました。

『自由度が高い=ユーザーに優しい』は、嘘だった。

未経験ユーザーに『失敗できる選択肢』を渡してしまうと、彼らは無自覚に失敗します。SÉtelierのターゲットは『リラックスしたい人』なので、『どうやっても気持ちいい音楽が作れる』状態の方が圧倒的に良いUXだった。

『制約こそが優しさ』というのは、デザイン界隈ではよく言われる原則だそうですが、未経験者がそれを実感する瞬間は、こういうところにあるんだなと思いました。

💡 学び:制約こそが、未経験ユーザーへの優しさ。 「自由度」と「使いやすさ」は別物。

ちなみに、有料版では『全音階モード』を選べるようにしてあります。慣れた人には自由を、初心者には安心を。


地雷4:reanimatedを入れて、わざわざ削除した話

最後の地雷は、ちょっと毛色が違います。『AIが推した依存ライブラリを、自分で削除する判断をした話』です。

React Native Reanimated とは

最初の技術選定の段階で、Claudeから『React Native Reanimated』というアニメーションライブラリの導入を提案されました。

「滑らかなアニメーションには Reanimated が定番です。標準のAnimated APIより60FPSで動きやすく、複雑な演出にも対応できます」

確かに人気ライブラリで、ネット上でも『Reanimated を使え』と書かれている記事が多い。僕は素直に従って導入しました。

何が起きたか

ところが、Reanimatedはネイティブモジュールに依存していて、Expoの設定ファイルに babel.config.js でプラグインを追加する必要がある、Development Build を作り直す必要がある、など『未経験者には複雑すぎる手順』が次々に必要になりました。

しかも、ビルドが時々失敗する。エラーメッセージはReanimated関連だけど、原因は他のモジュールとのバージョン競合だったり、設定ファイルの書き方だったり。

ある時、ふと考えました。

「そもそも、僕のアプリで Reanimated じゃないとダメな演出ある?」

オトダマのぽよん、ボールの軌跡、ヒット時の光のエフェクト――どれも『標準のAnimated APIで十分』なシンプルなアニメーションでした。Reanimated の高機能はオーバースペック。

引き算する判断

そこで、思い切って『Reanimated を削除して、標準の Animated APIだけで書き直す』判断をしました。

結果:

  • ビルドが安定
  • 設定ファイルがシンプルに
  • アプリの容量が減少
  • アニメーションは見た目変わらず

『AIが推したから入れたけれど、自分のユースケースには不要だった』という典型例でした。

AI時代の依存ライブラリとの付き合い方

連載を通じて気づいてきたのですが、AIは「ベストプラクティス」を提案することが多いんですね。Reanimatedも「定番だから」「ベストプラクティスだから」提案された。

でも、ベストプラクティスは『大規模・複雑な開発でこそ価値を発揮する』ものが多い。
個人開発のシンプルなアプリには、過剰装備になることがあります。

この経験から、僕は『AIが推したライブラリでも、本当に必要かを自分で見直す』ようになりました。第2回の『AIに従うのも、AIに従わないで撤退するのも自分で決める』の延長線上です。

学び:AIが推した依存ライブラリも、必要なければ削除する勇気を持つ。 「引き算」は AI時代の重要なスキル。


補足:Zustandのrace condition問題(コラム)

これは技術的な詳細になるので軽く触れるだけにします。

SÉtelierでは状態管理に Zustand というライブラリを使っているのですが、開発初期に『オトダマを連続で配置すると、たまに1個消える』というバグが発生しました。

原因は『非同期で複数の状態更新が走ったときに、後から来た更新が前の更新を上書きしてしまう』というレース・コンディション(race condition)。

解決策としては、状態を更新するときに set(s => ...) という『コールバック式の書き方』にすれば、前の状態を必ず引き継いでから更新するようになり、バグは消えました。

これも未経験者には『なぜ動くのか/なぜ動かないのか』が直感的には分からない種類の問題です。Claudeに教えてもらわなければ、何時間溶かしても解けなかったと思います。


まとめ:未経験者+AI開発で気付いた共通パターン

第3回の地雷たちを並べてみると、ある共通パターンが見えてきます。

『未経験者には予測も分析もできない問題が、必ず発生する』

そして、それを解決する流れも共通でした。

  1. 何かが「変だ」と気づく
  2. Claudeに状況を説明する
  3. 「これは○○の問題ですね」と原因を教えてもらう
  4. 解決策を実装する
  5. 直る

ここでの未経験者の役割は、『状況を正確に伝えること』『判断すること』だけです。AudioContext制限を知っている必要はないし、ペンタトニックスケールを最初から知っている必要もない。『気づいて、聞いて、判断する』ができれば、AIが知識を埋めてくれる。

これが、未経験者がAIと一緒にアプリを作れる時代の本質だと思っています。


次回予告

第4回は『RevenueCatとAdMobで個人開発を持続可能にする』です。

個人開発で一番不安なポイントの一つ、『課金実装』。月額・年間・買い切りの3本立てにした思想、AdMobとリワード広告で『15分Pro体験』を実装した話、無料/Pro機能の線引きなど、ビジネスサイドの実装をまとめます。

これも未経験者目線で『なぜRevenueCatを選んだのか』を中心に書きます。

追記:更新しました↓


iOSアプリ開発備忘録(もとい、SÉtelier 開発記) 第3回 / 全5回

↓公開したアプリのリンクです(興味あれば!フィードバックも大変有難いです!)↓

🎵 SÉtelier(セトリエ)を試してみる

音を置く、自分だけのアンビエントBGM。
無料でダウンロードできます(追加コンテンツや便利機能のアンロックあり)

App Storeで開く

コメント

タイトルとURLをコピーしました