Leon . Kang
Back to articles
💻 Coding 2/25/2026 8 min read

画面を埋め尽くす #if os(macOS) からの脱却:SwiftUI マルチプラットフォーム開発の「陣痛と再誕」の記録

Apple が SwiftUI を発表したとき、「Learn once, apply anywhere」(一度学べば、どこでも適用できる)という隠れたスローガンは、多くの開発者にとって究極の夢となりました。私自身のプロジェクトでも、最初は同じように考えていました。結局のところ、iOS、iPadOS、macOS で同じコードを動かせることは、究極の効率化ツールに思えたからです。

しかし、プロジェクトが成長するにつれ、この一見完璧な「クロスプラットフォーム理論」は実践において厳しい教訓を私に与えました。今日は、このプロジェクトがいかにして当初の「ごちゃ混ぜ」状態から混乱に陥り、そして最終的に「精緻な分離」へと向かうリファクタリングの決断に至ったのかを振り返ります。

1. ハネムーン期:利便性を優先した単一 Target の落とし穴

プロジェクトの立ち上げ当初、リリースのスピードと開発の容易さを優先し、最も直感的な決定を下しました:iOS、iPadOS、macOS を全く同じ Target の下に配置することです。

当時の考えは単純でした。SwiftUI はクロスプラットフォームなのだから、macOS 用には Mac CatalystDesigned for iPad をチェックするだけで、iPad のレイアウトとインタラクションロジックをそのまま Mac で再利用できるのではないか?

当初の開発体験は確かに「快適」でした。カラーリストや詳細ページを一度書けば、全プラットフォームでそのままビルドして動く。まるで開発効率の魔法を手に入れたかのような気分でした。しかし、すべての「安直な」アーキテクチャがそうであるように、これは嵐の前の静けさに過ぎませんでした。macOS 版を使い込むにつれて、あちこちに違和感が現れ始めました:

  • 不自然に広い余白
  • 無理やりタッチ操作を模したスクロール操作
  • マルチカラムナビゲーションではなく、全画面の Push 遷移
  • デスクトップ特有のツールバーやショートカットキーの欠如……

Mac で動くそれは、きちんとしたデスクトップソフトウェアというよりは、不格好な「スマートフォンの拡大シミュレータ」のように見えました。

2. 応急処置と妥協:Shared 層の抽出と悪夢の始まり

Mac のデスクトップユーザーをこれ以上ないがしろにはできないと悟り、プロジェクトに手を加えることにしました。

アーキテクチャ的には、合理的な最初の調整を行いました:macOS 専用の Target を新設し、データベースモデル(Data Model)やコアビジネスロジック(Service)など、UI に依存しないコードを Shared 層(共有コンポーネントライブラリ)として切り出しました。これは Apple が公式に推奨している標準的なアプローチです。

しかし――これが最も後悔することになる決定でしたが――「コードの再利用を最大化する」という名目のもと、ビュー層で手を抜き、両方のプラットフォームで同じ UI View ファイルを共有し続けてしまったのです。

レイアウトの違いはどう解決したか? 簡単です。違和感のある場所に条件付きコンパイルを追加していきました。マクロが至る所に現れ始めました:

#if os(macOS)
// Mac 用のボタン追加、フォント変更、デスクトップ専用の PickerStyle を使用
.pickerStyle(.radioGroup)
#else
// iOS 用のスタイル
.pickerStyle(.segmented)
#endif

最初は数箇所だったので我慢できましたが、いくつかのマクロでクロスプラットフォームの差分を吸収できていることに、当時はまだ密かに満足していました。

3. 技術負債の爆発:if os(macOS) に支配される恐怖

ページが増え、色抽出、プロフェッショナルなパレット編集、カメラによる動的な色取得などの機能が複雑になるにつれて、これらの「共有 UI ファイル」は完全に沼地と化しました。コードは #if os(macOS) という蔓(つる)で埋め尽くされました。

この高度に結合した「ハイブリッドなコード」は、いくつかの壊滅的な結果をもたらしました:

  1. 可読性が底をつく:本来なら 100 行で簡潔に書けるはずの View が、条件付きコンパイルによって 300 行以上に膨れ上がりました。コードを読む際、脳はコンパイラのように常にコンテキストを切り替え、不要なブランチを削ぎ落とす必要があり、精神的な負担が凄まじいものになりました。
  2. UI が極めて不安定になり、一箇所直すと他が壊れる:共有ファイルのレイアウトを変更するのは、まるで地雷原を歩くようでした。SwiftUI のレンダリングにおいて iOS(UIKit ベース)と macOS(AppKit ベース)には微妙な違いがあるため、一方は直っても他方が壊れるという「いたちごっこ」が頻発しました。今日 iOS のパディングを直せば、翌日には Mac の表示が謎の崩れを起こすのです。
  3. 開発效率の逆転:コードを減らすための工夫だったはずが、皮肉にもプロジェクトの後半では、そのほとんどの時間が両プラットフォームの互換性調整、トラブルシューティング、バグ修正に費やされるようになりました。新しいコンポーネントを一つ追加するのにも、戦々恐々とする日々でした。

4. 抜本的な改革:物理的な分離と Native への回帰

限界でした。プロジェクトの将来の保守性のために、ついに大規模なアーキテクチャの「手術」を決断しました:UI 層レベルでの再利用という幻想を捨て、完全に物理的な隔離を行うことにしたのです。

リファクタリングの核となる戦略は明快かつ断固としたものでした:

  • Shared コアを堅守する:データ層、状態管理、各種 Service などの非 UI ロジックは引き続き Shared 層に残しました。これまでの検証から、これらのビジネスロジックは両プラットフォームで高度に一致していることが証明されていたからです。
  • ビュー層を完全に切り離す:すべてのハイブリッドな View ファイルを「解散」させました。iOS と iPadOS は従来の View システムを維持し、macOS 用には Mac プレフィックスを冠した専用の View セットを新設しました(例えば、ごちゃ混ぜだった PhotoPalettePickerView を、純粋な iOS 版と純粋なデスクトップ版の MacPhotoPalettePickerView に分離しました)。
  • Native な体験を解禁する:分離後、Mac 側のページはついに束縛から解放されました。マウス操作に最適化されたホバー状態(Hover)や、ネイティブなサイドバー(Sidebar)、マルチウィンドウ管理などを、iOS のジェスチャー操作に縛られることなく自由に取り入れることができるようになりました。

まとめ:二回書くべきコードは、惜しまず書く

「Write once, run anywhere」という言葉は、シンプルなツールアプリや純粋なロジックにとっては美しい理想です。しかし、複雑なインタラクション、高度にカスタマイズされたコントロール、そして洗練されたユーザー体験を求める商用レベルのアプリケーションにおいては、無理な共通化はプロジェクトを蝕む原因となります。

SwiftUI のマルチプラットフォーム開発において、真の指針とすべきは 「Learn once, write anywhere」 です。

適度な UI コードの冗長性は、悪いことではありません。むしろ、プロジェクトの安定性、今後の拡張性、そしてユーザーに各プラットフォームでの究極の「ネイティブ体験」を提供するための不可欠な基盤です。コードを分けることを恐れてはいけません。隔離が必要なときは、迷わず物理的に隔離してください。それは製品に対する責任であるだけでなく、将来のあなた自身の「髪の毛」を守ることにも繋がるのです。