Objective-C
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
はじめに こんにちは、ZOZOTOWN開発2部iOSブロックのらぷ( @laprasdrum )です。普段はZOZOTOWN iOSアプリを開発するチームで各メンバーの開発における設計や技術課題のフォローアップを担当しています。また、iOS領域におけるテックリードとして社内の技術共有会や ZOZO.swift などを運営しており、各プロダクトのiOSチーム全体をつなげる横断活動に従事しています。 ZOZOTOWN iOSアプリは2010年11月にリリースされ、15年以上にわたって開発が続くプロダクトです。長い歴史の中でチームと技術が変遷し続け、Fat ViewControllerやObjective-Cコードの残存といった技術的負債を抱えていました。これに対してチームは2023年からアーキテクチャの刷新に本格的に取り組んできました。 本記事では、その3年間の変遷を振り返り、アーキテクチャがどのように進化し、設計をレビューする力がチーム全体にどう広がったかをお伝えします。 なお、チーム運営の全体像は ZOZOTOWNのiOSチームを支えるチーム運用 で紹介しています。Fat ViewController解消の具体的な手法は ZOZOTOWN iOSアプリでのFat ViewController解消への取り組み を参照してください。 目次 はじめに 目次 アーキテクチャ変遷の全体像 最初の一歩 — 2023年の型作り MVVMの採用と方針ドキュメントの策定 アーキテクチャレビューの始動 意思決定を支えた二層構造 アーキテクチャの判断と見直し レイヤー設計の選定と見直し Translatorレイヤーの導入(2024年5月) MVVM + UseCaseの採用(2024年9月) ドメインレイヤーの廃止とRepositoryパターンへの集約(2026年3月) 開発プロセスの進化 定量データで見るチームの変化 アーキテクチャ移行の規模 設計レビューの担い手の逆転 AIによる設計知識のチーム展開 1. Geminiによるレビューコメントの要約 2. Copilotレビュー指示の導入・強化 3. チーム共有コマンドへの昇華 4. アーキテクチャ学習サイトと理解度クイズ まとめ アーキテクチャ変遷の全体像 各トピックの詳細へ入る前に、3年間の変遷と技術スタックの移行を俯瞰します。 まず、主要な取り組みの時系列です。 2023年 — MVVM化の型作り Fat ViewControllerの解消を目指し、対象画面を決めてリファクタリングを実施した。 2024年前半 — Translatorレイヤーの導入・アーキテクチャレビューの再設計 API通信モジュールとUIレイヤーの依存を断ち切るTranslatorを導入した。PRレビュー課題の体系的分析から、関心の分離をレビューする運用に刷新している。 2024年後半 — MVVM化の複数画面への展開・Objective-CからSwiftへの移行 商品詳細を中心にMVVM + UseCaseを適用した(詳細は 前述の記事 を参照)。 2025年 — 大規模画面の並行リファクタリング 大規模画面にMVVM + UseCaseを適用し、Storyboardの一部削除やBaseViewControllerの廃止を進めた。 2025年後半 — ガイドラインと設計制約の明文化 MVVM + UseCase統一ガイドやアーキテクチャガイドラインを策定した。 2026年〜 — Repositoryパターン本格導入 実装経験を経てUseCaseが不要なケースを特定し、Repositoryパターンを本格適用している。 技術スタックも同時期に大きく変わっています。 UIアーキテクチャ(2023〜2026) MVC(Fat ViewController)からMVVM + Repositoryへ移行した。 UIフレームワーク(2023〜2026) UIKit + XIB/StoryboardからUIKit(コードベース)+ SwiftUIへ移行した。 非同期・リアクティブ(2024〜2026) ReactiveSwiftからCombineへ移行し、非同期処理にSwift Concurrencyを導入した。 言語(2023〜2025) Objective-CとSwiftの混在状態からSwift中心へ移行した。Swift比率は94.7%(2023年)から99.5%(2026年)へ向上している。数値上は約5ポイントの変化だが、2025年に削除した12クラスはのべ64箇所から参照されており、1クラスの削除に最大17ファイルの改修を伴った。通常の案件開発と並行しながら段階的に解消した。 テスト(2025〜2026) XCTest + NimbleからSwift Testing + 振る舞い駆動のテスト記述へ段階的に移行している。 これらの移行は一括で実施したのではなく、チームの習熟度に合わせて段階的にスコープを拡大していきました。その意思決定の裏側を以降のセクションで掘り下げます。 最初の一歩 — 2023年の型作り MVVMの採用と方針ドキュメントの策定 Fat ViewControllerの解消にあたり、チームはアーキテクチャとしてMVVMを採用しました。実装に先立ち、2022年末からMVVMの実装方針をチームで統一するためのドキュメント作成が始まり、2023年初頭にWIPを外してチームに公開されました。これにより、MVVM化の共通認識となる土台が整いました。このドキュメントは以降も継続的に更新されています。 アーキテクチャレビューの始動 方針が共有された直後の2023年3月、最初のMVVM化が始まりました。この取り組みで特に意識されたのは、単なる1画面の改修ではなく「今後他の人がMVVMで画面を実装する際の参考になるか」という点です。 実際にこのレビューには25件のアーキテクチャレベルのコメントが付きました。レビュアがデータフロー図を描いてViewModelの状態更新フローを可視化するなど、コードではなく設計を先にレビューする文化の萌芽となっています。 続く2画面目(2023年4月)では、レビュー依頼の本文に設計図を含む「アーキテクチャレビュー」セクションが初めて明示され、テンプレートとして定着しました。 こうしてMVVM化の最初の型は作れたものの、これを複数画面に展開し、チーム全体で設計判断の質を維持していくには、個々のPRレビューだけでは限界がありました。チームとして設計を議論し、方針を決め、知識を共有する仕組みが必要だったのです。 意思決定を支えた二層構造 チーム運用の記事 で紹介した「開発生産性MTG」と「Rethink!」について改めて簡単に説明します。 開発生産性MTG :シニアエンジニアとマネージャーが主導し、コードベースの技術的課題を除くチームの課題を議論する場である(2023年10月〜、約30回開催)。各案件の開発マネジメント、PRレビュー、メンバー間の情報格差など、チームがより成熟するために必要な課題を整理し、アクションを設計していた。 Rethink! :技術的な課題をチームメンバーで定期的に見直す週次勉強会である(2024年2月〜通算90回以上)。技術的負債と感じていること、Appleが提示している技術への所感や適応方針、自分だけしか知らないかもしれないことなど、お題は多岐にわたる。話した内容はアーカイブとして残し、過去の意思決定の資料として参照している。 この2つは、案件開発の流れの中だけで方針を決めず、立ち止まって振り返り分析するためのイベントです。 なお、現在は開発生産性MTGという場を設けなくても、メンバー各々がときに立ち止まり、チームに課題を投げかけるコミュニケーションが日常的に行われるようになりました。立ち止まって考える姿勢が特定の会議体に閉じず、チーム全体に浸透した結果、開発生産性MTGは役割を終えています。 この二層構造から3つの重要な方針が生まれました。コード課題の根本原因の特定、リファクタリングのゴール定義、そしてレビュー工程の分離です。 まず、コード品質の課題を時間をかけて分析し、関心の分離・疎結合がコード課題の原因の8割という結論に到達しました。暗黙知が多く「良いコードとは何か」の定義が揃っていないことが本質的な問題であり、「きれいなコードの定義」を目指すのではなく「アンチパターンの提示」で十分という方針が生まれました。 この分析を受けて、リファクタリングのゴールを「きれいなコードをメンバー全員が理解すること」と定義しました。リリースすることはMUSTではなく、認識を揃えることが目的です。この方針のもと、Rethink!をチーム全体での実践場として活用することが決まりました。 同時に、レビューが特定のメンバーに集中しマージ待ちが常態化していた問題も分析しました。その結果、外部品質(仕様通りに動くか)と内部品質(設計・保守性)の確認がひとつのレビューに混在していることが負荷の原因と判明しました。これを受けて、基本設計レビュー(仕様の漏れと実現可能性)、アーキテクチャレビュー(レイヤー間の関心分離)、コードレビュー(実装)の3段階にレビュー工程を分離しました。 これらの方針は、Rethink!を通じてチーム全体の実践に落とし込まれました。全員が同じ題材で設計を提出して観点を揃えたり、実際のPRを題材にレビューの進め方を学んだりしました。レイヤー設計やレビュー手法といったアーキテクチャの具体的な意思決定もRethink!から生まれています。 開発生産性MTGが方向性を定め、Rethink!がチーム全体で実践し、ドキュメントとして蓄積していくことで、暗黙知が特定の個人に閉じることなく、チーム全体の形式知として更新される構造を実現しました。PRレビューのコメントでも日常の雑談でも、「Rethink!で話してみますか」という言葉が自然に出てくるようになりました。 アーキテクチャの判断と見直し 二層構造の仕組みが実際にアーキテクチャの判断をどう動かしたのか、レイヤー設計と開発プロセスの2つの観点から見ていきます。 レイヤー設計の選定と見直し チームはレイヤー構成を3回にわたって選定・見直しました。 Translatorレイヤーの導入(2024年5月) 「関心の分離が課題の8割」という方針を受けて、最初に着手したのはデータレイヤーの整備でした。ZOZOTOWN iOSでは、API通信の処理をアプリ本体とは独立したモジュールとして切り出しています。このモジュールがAPIリクエストの発行とレスポンスのデコードを担い、アプリ側はモジュールが提供するクライアントを呼び出す構成です。 しかし当時は、このAPI通信モジュールが返すレスポンス型をViewModelがそのまま扱っており、モデル変換のロジックがUIレイヤーに漏れ出していました。UIレイヤーがAPI通信モジュールの型に直接依存する状態です。 この課題に対して導入されたのがTranslatorレイヤーです。API通信モジュールのレスポンスをアプリ内のモデルに変換する責務を一手に担い、UIレイヤーとAPI通信モジュールの依存を断ち切りました。2024年5月に最初の実装がマージされ、同時期にMVVM方針ドキュメントにもTranslatorの項目が追記されています。 この分離は設計方針の浸透とテスタビリティの両面で効果がありました。ViewModelはアプリ内のモデルだけを扱う前提になるため、「ViewModelはアプリ内のモデルを扱う層である」という方針をチームに伝えやすくなりました。また、ViewModelのテストからAPI通信モジュールへの依存がなくなり、テストビルド時に不要なモジュール依存を削除できるようになりました。 Translatorの導入により、データレイヤーの一部が確立されました。次の課題は、ViewModelとこのTranslatorの間、つまりドメインレイヤーをどう設計するかです。 MVVM + UseCaseの採用(2024年9月) データレイヤーやドメインレイヤーの責務分割にはチーム方針がなく、メンバーによるコード品質の差が大きくなるポイントでした。一方で、馴染みのないアーキテクチャを導入してメンバーの認知負荷を上げるわけにもいかず、最低限のレイヤー定義が求められました。開発生産性MTGでUseCaseの責務を議題に絞り込み、Rethink!で3つの選択肢を比較しました。 Androidの推奨アプリアーキテクチャ方式 :UseCaseはオプショナルで、複雑なビジネスロジックのカプセル化や、複数のViewModelから再利用されるロジックの共通化を担う。ただしデータレイヤーのRepositoryがある前提 Clean Architecture + DDD方式 :UseCaseがドメインロジックを担い、ドメインモデルとペアで設計する。画面に紐づかずドメイン単位で定義するため、境界設計が難しくなる MVVM + UseCase方式 :UseCaseがドメインレイヤー(本来のUseCase)とデータレイヤー(Repository)の両方をカバーする。データソースが単一ならRepositoryを別途作るコストをスキップできる チームが選んだのはMVVM + UseCaseでした。ZOZOTOWN iOSではデータソースが単一のケースが多く、Repositoryを別途設ける必要性が低かったためです。「レイヤーの責務が肥大化しすぎた場合はClean Architecture方向に進化させる」という留保付きの判断でした。 ドメインレイヤーの廃止とRepositoryパターンへの集約(2026年3月) この判断のもと、チームは複数画面でMVVM + UseCaseを実装していきました。しかし約1年半の運用を経て、Rethink!で「ドメインレイヤー(UseCase)を廃止し、データレイヤー(Repository)に責務を集約すべきではないか」という議論が持ち上がりました。実装を重ねる中で見えてきたのは以下の点です。 ZOZOTOWN iOSではドメインモデルやそれを用いるビジネスロジックが稀であること APIClientとTranslatorの組み合わせが実質的にRepositoryの役割を果たしており、UseCaseとの責務が重複していたこと UseCaseを設けても責務の理解が揃わず、チーム内に混乱を招いていたこと 重要だったのは「各レイヤーの責務の理解が揃うこと」であり、UseCaseという層を設けること自体が目的ではなかったという気づきです。結果、MVVM + Repository(UseCaseなし)をチームの方針としました。 これは単なる揺り戻しではなく、「レイヤーの責務が肥大化しすぎた場合はClean Architecture方向に進化させる」という最初の留保に対する回答です。実際に使ってみた結果「肥大化ではなく責務の重複が問題であり、むしろ層を減らすほうが適切だった」という結論に至りました。 開発プロセスの進化 同時期に、設計の進め方そのものにも変化がありました。 アーキテクチャレビューでは、当初は重厚なPlantUML図を用いて設計を可視化していました。しかし「作図コストが高い割に手戻りは防げない」ことがわかり、Protocol定義をPRにして設計をレビューする軽量な手法に転換しました(2024年8月)。XIB/Storyboardからの移行方針(2025年4月)など、UIフレームワークの選定もRethink!で議論しています。 こうした議論と意思決定の積み重ねは、チームの活動にどのような変化をもたらしたのでしょうか。定量データで確認します。 定量データで見るチームの変化 アーキテクチャ移行の規模 ViewModel / UseCase / Repository / Translatorを含むコミットメッセージを年別に集計しました。 年 コミット数 前年比 2023 217 — 2024 302 +39% 2025 608 +101% 2026(〜2月) 194 — コミット数の増加と並行して、レガシーコードの削減も進みました。以下は2025年のレガシーコード削減実績です。 指標 2025年1月 2025年12月 削減率 Objective-C .mファイル 19 3 84% Objective-Cコード行数 2,768 788 72% XIB/Storyboard 88 58 34% 特筆すべきは、2024年に積み重ねた依存解消が2025年の大規模削除を可能にした点です。一度に大きく変えるのではなく、依存を個別に剥がし続けたことで、翌年のレガシーコード削減につながりました。 設計レビューの担い手の逆転 アーキテクチャ移行の規模が拡大する中で、その設計レビューを誰が担うかにも大きな変化が起きました。 2023年末時点では、GitHubのCODEOWNERSに筆者のみが設定されており、すべてのPRが筆者のApproveなしにマージできない状態でした。この期間にマージされたPRは月平均約53件にのぼります。一部案件のスプリントレビュー時にレビュー待ちチケットが残留し、「マージ待ちになっている時間が結構ある」という声が上がるほどで、まさに「できる人がやる」体制の限界でした。 2024年6月、この問題を受けて筆者ともう1名のシニアエンジニアによる2名体制に移行し、アーキテクチャレビューとコードレビューの役割を分担しました。同時に、それまでレビューに関わる機会のなかった他のメンバーをピアレビュアとして外部品質のレビューに参加してもらう体制を整え、レビュー文化の醸成とレビュア育成がここから始まりました。 PRレビューコメントのうち設計に関するものを定量分析した結果、シニアレビュアの比率は2024年10-12月の89%から2025年1-3月には25%へ低下しました。2026年1-2月時点では9%まで下がり、ピアレビュアが設計コメントの91%を担っています。2026年2月には固定レビュア制度そのものが廃止され、ピアレビュアのランダム選出に移行しています。 この変化の背景には、開発生産性MTGによる制度設計とRethink!での実践という二層構造の仕組みがありました。 数字の裏にある変化を一人のメンバーの軌跡で見ると、より具体的になります。初期(2024年)はレイアウト設計の範囲内にとどまっていたレビューが、UseCase設計の責務境界の理解(2025年1-3月)を経て成長しました。最終的にはディレクトリ構造・命名規則の統一提案・APIレスポンス型の設計方針など、プロジェクト全体を俯瞰するレビューに到達しています。 設計レビューの担い手が広がる一方で、シニアレビュアが持っていたレビュー観点そのものを、人に依存しない形で残す取り組みも並行して進めました。 AIによる設計知識のチーム展開 アーキテクチャの形式知化の最終段階として、設計知識とレビュー観点をAIを活用した4つの手段でチームに展開しました。 1. Geminiによるレビューコメントの要約 レビュア育成とシニアレビュアへのレビュー依存の解消を目指し、週次でPRレビューコメントを収集しGeminiに要約させてSlackに投稿しています。要約結果をチームの週次定例で振り返り、レビュー観点の共有やレビュア同士の知見交流、まだレビュアになっていないメンバーへのレビュー観点インプットに活用しています。 2. Copilotレビュー指示の導入・強化 GitHub CopilotによるPRへのインラインコメントは、開発者が環境構築や新しいツールを導入する必要がなく、チームメンバーがAIの恩恵を素早く実感できる手段でした。 copilot-instructions.md にレビュー観点を定義し、アーキテクチャ準拠・iOSベストプラクティス・テスタビリティ等の知見を追加しました。 Copilotによるアーキテクチャ関連コメントは月間28件から94件に増加しました(2025年8月から2026年2月)。従来はシニアレビュアが暗黙的にチェックしていた観点が自動的にレビューされるようになりました。 3. チーム共有コマンドへの昇華 レビュー観点をClaude Codeのコマンドとして言語化し、チーム共有のセルフレビュースキルとして整備しました。マージ先ブランチの確認、開発チケットや設計ドキュメントからのコンテキスト取得、アーキテクチャ準拠チェック、動作確認シナリオの生成まで、PR作成前に開発者自身でセルフレビューできるようにしました。 4. アーキテクチャ学習サイトと理解度クイズ アーキテクチャガイドラインやテンプレートの内容をもとに、インタラクティブな学習サイトの生成とCLI上でのクイズによる理解度チェックをClaude Codeのコマンドとして整備しました。レイヤー構成やデータフロー、アンチパターンなどをブラウザ上で視覚的に学べるほか、クイズではドキュメントに明記されたルールから出題し、回答ごとに根拠となるドキュメント箇所を提示します。 この4つは、暗黙知の移転経路がそれぞれ異なります。 Gemini要約 :人のレビューコメントをAIが集約して全員に届ける仕組み Copilot指示 :人からAIへの知識の埋め込みによる自動レビュー セルフレビューコマンド :人がAIを介して自分自身に観点を返す仕組み 学習サイトとクイズ :ドキュメントをAIが対話的な教材に変換する仕組み 経路が異なるからこそ、どれかひとつが欠けても他で補完できる構造になっています。 まとめ ZOZOTOWN iOSチームのアーキテクチャは、2023年の1画面でのMVVM化から始まりました。2024年のTranslator導入と複数画面への展開、2025年の大規模並行リファクタリング、2026年のRepositoryパターンへの集約と段階的に進化しています。チームの習熟度に合わせてスコープを拡大し、実装経験をもとにレイヤー構成そのものを見直す判断もできるようになりました。 この過程を支えたのが、開発生産性MTGで方向性を定め、Rethink!でチーム全体が実践し、ドキュメントとして蓄積していく二層構造の知識形成フローです。暗黙知が特定の個人に閉じることなく、チーム全体の形式知として更新される仕組みを作りました。 さらに、Geminiによるレビューコメント要約、Copilotレビュー指示、Claude Codeのセルフレビューコマンド、アーキテクチャ学習サイトの4つを整備しました。これらを通じて、設計知識とレビュー観点をAIを介してチームに展開しています。 設計レビューの担い手もシニアレビュア89%からピアレビュア91%へ完全に逆転しています。 この変化はチームに具体的な効果をもたらしています。責務やデータフローの定義がチーム全体で揃ったことで、PRレビューの場で合意形成がスムーズに進むようになりました。設計の相談先が特定の個人に閉じなくなり、意思決定の属人化も解消されています。かつては口伝に頼っていた知識が体系化されたことで、新メンバーが自律的に学べる環境も整いました。 「できる人がやる」から「全員で設計をレビューできる」への転換が、アーキテクチャの一貫性とチームのスケーラビリティの両立を実現しています。 ZOZOでは一緒に働くエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
本記事は【 Advent Calendar 】の9日目の記事です。 AI戦略室のM・Wです。この記事を書いているのは12月1日です。 11月30日にジャパンカップで カランダガン の単勝と三連単を取りました。現地で観戦をしていたのですが、大変興奮したレースでした。ゴール間際の2頭の叩き合いに混ざる空馬。 ぜひYoutubeで動画を見てください。以上、12月11日の記事でした! 沼の淵 突然ですが、forkやspawnの違いについて知っていますでしょうか。 私は知らなかったです。いまも正直わかりません。 その結果、沼にハマってしまいました。抜け出せないまま今日を過ごしています。 これは自分用のメモ+同じようなことを実装する人がいた際の一助になればいいなと思って執筆しています。 まずとあるAgentを考えます。このAgentは「環境」を知覚し「行動」を決定するAgentです。 Agentは「行動」を行うと「環境」から「報酬」を手にすることができ、「環境」は次の「状態」に遷移します。 図にするとこんな感じです。これは基本的な強化学習を説明する際に用いられる簡略図です。 今回、ボン◯ーマンのようなPvPをするゲームについて、強化学習を使ってエージェントを作る機会がありました。つまり上の画像に照らし合わせると下記の表となります。 用語 対応 環境 ゲーム自体 エージェント 操作キャラクター 状態 その時時の盤面 行動 移動+ボム配置+キック 報酬 勝利/敗北など レギュレーションとして自陣営は2つのBotを用意する2vs2の対決と提示されたため、上記のエージェントが2つ必要なマルチエージェント学習を行う必要がありました。 (ちなみに特に実装は指定されなかったのでロジックで記述するでもOKです。AI部署なので強化学習で挑みました) 沼にダイブ ここでいよいよ掲題に出てきたマルチプロセス学習の話ができてます。 最新の強化学習アルゴリズムに疎かったので、一旦Actor-CriticベースのA3Cアルゴリズムを選択しました。実装したモデル構造は下記となります。 盤面のエンコードやモデル設計については以下のモデルを参考としています。Cursor先生ありがとう! Multi-Agent Training for Pommerman: Curriculum Learning and Population-based Self-Play Approach: https://arxiv.org/abs/2407.00662 マルチプロセス学習の全体感としては下記の図です。 ゲーム盤面を提供するホスト側とエージェント間はWebSocketによる疎通を行う必要があるため、各BotごとにWebSocketサーバを子プロセスとして作成します。 つまり子プロセス上では、ゲーム開始から終了まで実行されますが、そこで収集したデータについては何かしらの方法を使って親プロセス上に移動させないと、子プロセスがterminateされた際に破棄されてしまう問題があります。 そのため 共有メモリ を使って誤差勾配を親プロセス側と共有を行い、最適化器を使って共有モデルのパラメータを更新、次の学習ループで子プロセスへ配分を繰り返し学習を行っていきます。 沼の底 学習モデルを共有メモリに配置するためには model.share_memory() を実施しますが、今回の実装のケースにおいてたびたび子プロセスの model.backward() が失敗する事象がありました。 いくつか問題はありましたが、 ・下記のログが出力され、backward処理が失敗するケース objc[8623]: +[MPSGraphObject initialize] may have been in progress in another thread when fork() was called.objc[8623]: +[MPSGraphObject initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug. ・backward()が呼び出された際に子プロセスがクラッシュして音もなく消える 使用してる端末がMacOSであるため、前者のケースでは $export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES を実行することでエラー文が出力されなくなります。しかしながらこれはワークアラウンドとなります。 MacOSでは fork() が実行された際に、親プロセスで既に初期化されつつあったObjective-Cのデータ構造が子プロセスに引き継がれると、子プロセス側でその初期化が安全に完了できない状況が発生することがあります。これにより、子プロセスがクラッシュ(通常はEXC_BAD_ACCESS)したり、デッドロックしたりします。 Pythonのmultiprocessingは、デフォルトでは「fork」方式を使って新しいプロセスを生成するため、この問題に直面しやすいです。そのため2点目の音もなく子プロセスが消えた原因も含めて、backward()処理がうまく動作しないのは fork() による子プロセスの生成に問題があると結論付けました。 ちなみにPyTorchからマルチプロセス学習する際のベストプラクティスでも「Use an alternative process start methods, such as spawn or forkserver, which ensures a clean initialization of each process.」と記載があります。 https://docs.pytorch.org/docs/stable/notes/multiprocessing.html#poison-fork-in-multiprocessing spawn は fork とは違い、Pythonインタープリタごとプロセスを生成します。起動が遅いというデメリットはありますが、必要な情報のみを親プロセスから引き継ぐ点や上記の初期化に起因するようなエラーを回避できるメリットがあります。 実験 簡単なNNを構築してspawnを使用したbackward()がうまくいくかどうかを実験してみます。 import torchimport torch.nn as nnimport torch.multiprocessing as mpimport torch.nn.functional as Fimport torch.optim as optimclass ExampleNN(nn.Module): def __init__(self): super(ExampleNN, self).__init__() self.fc = nn.Linear(10, 1) def forward(self, x): out = self.fc(x) return F.sigmoid(out)def train(xs, target): model = ExampleNN() optimizer = optim.SGD(model.parameters()) out = model(xs) loss = F.binary_cross_entropy(out, target) optimizer.zero_grad() loss.backward() optimizer.step() print('trainメソッドが終了したよ') 上記のtrainメソッドをforkで動かしてみます。 if __name__ == '__main__': x = torch.rand(1, 5, 10) t = torch.Tensor([0, 1, 1, 0, 1]).reshape(1, 5, 1) train(x, t) print('-'*54) ctx = mp.get_context('fork') processes = [] for i in range(5): p = ctx.Process(target=train, args=(x, t)) p.start() processes.append(p) for p in processes: p.join()>>> trainメソッドが終了したよ------------------------------------------------------RuntimeError: Unable to handle autograd's threading in combination with fork-based multiprocessing. See https://github.com/pytorch/pytorch/wiki/Autograd-and-Fork 無事エラーが出ました。 ちなみに途中の train(x, t) をコメントアウトすると正常に動作します。これは上記のGithubにも記載の通り、backward()で呼び出されているAutogradがすでに使用されている状態でfork()されたことに起因するエラーとなります。 Autograd engine relies on threads pool, which makes it vulnerable to fork. We detect such situations and warn users to use spawn method of multiprocessing. 指示通りspawnでやってみましょう。 if __name__ == '__main__': x = torch.rand(1, 5, 10) t = torch.Tensor([0, 1, 1, 0, 1]).reshape(1, 5, 1) train(x, t) print('-'*54) ctx = mp.get_context('spawn') processes = [] for i in range(5): p = ctx.Process(target=train, args=(x, t)) p.start() processes.append(p) for p in processes: p.join() >>> trainメソッドが終了したよ------------------------------------------------------trainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよ 無事エラーなく実行することができました。 この結果から spawn を明示的に指定することでbackward()処理のエラーを回避できることがわかります。 実際の実装は下記のような形となっています。 ctx = mp.get_context('spawn') # mp.set_start_method('spawn', force=True) # エピソードループ for episode in tqdm(range(num_episodes)): clients: List[BombermanClient] = self.create_clients() process_pool = [] for c in clients: p = ctx.Process(target=c.run) p.start() process_pool.append(p) # エピソードの学習 loop = asyncio.get_event_loop() loop.run_until_complete(self.train_episode(max_steps)) print("ロールアウト終了") # 子プロセスの終了を待つ for p in process_pool: if p.is_alive(): p.terminate() p.join(timeout=3) # 最大3秒待つ if p.is_alive(): # それでも終了しない場合は強制終了 print(f"Warning: Process {p.pid} did not terminate gracefully, forcing...") p.kill() p.join() そもそもMacOSだとデフォルトで spawn だそうです。つまりわざわざ明示的に fork を使ったところから全ては始まったのです。Cursorくん、君は頑なにforkを推していたよね・・・。 これで 戦 に臨めます! 終わりに 元々のスタート地点として、マイナビグループの中の一つである Mynavi Techtus Vietnam さんから挑戦状を叩きつけられたところからこの沼は始まりました。 Techtusさん「ボン◯ーマンAIを作成してPvPをやろう!それでどこのチームが強いか勝負だ!」 結果は・・・ 全敗 でした。 そもそもこの問題を解決したのは戦いが終わった3週間後ぐらいなんです! つまり、戦には裸同然で挑んだことになる・・・?すでに次の戦は始まっているという決意を胸に日々沼の中で生きたいと思います! イベント告知 12月23日にイベントを開催します!申し込みはこちらから▼ https://mynaviit.connpass.com/event/376769
はじめに こんにちは、WEARフロントエンド部iOSブロックの西山です。普段はWEAR iOSチームのマネジメント兼アプリの開発を担当しています。今年のWWDC25で、新しいソフトウェアデザインのLiquid Glassが発表されました。透明感のあるUIと流動的なアニメーションが特徴的なこの新デザインは、WEARアプリに大きな影響を与えました。鋭意進行中の取り組みとして、本記事では、Liquid Glass対応を計画的に進めるための取り組みを紹介します。 目次 はじめに 目次 Liquid Glassとは 課題 UIが大きく変わることでデザイン崩れが発生 タブバー タブバーの裏側にコンテンツが透過しない タブバーとコンポーネントが被る ナビゲーションバー ボタンに枠がつく ナビゲーションバーの下に配置してあるコンポーネントとの一体感を失う その他のコンポーネント Switchが大きくなったことによるレイアウト崩れ アラート(アクションシート)の変化 デザイナー、エンジニア、QAのコスト増大が予想される Liquid Glassの学習コスト 移行戦略 開発プロセスの整備 ブランチ戦略 Xcode 26対応 Xcode 16を使用している場合の例 Liquid Glassを無効にする Liquid Glass効果を動的に切り替えられるようにする Liquid Glass対応で分岐できるようにする 対応内容を一部紹介 contentInsetAdjustmentBehavior = .never の対応 contentInsetAdjustmentBehavior = .never の問題点 extendedLayoutIncludesOpaqueBars = true の対応 まとめ Liquid Glassとは iOS 26から導入される新しいデザインで、次の特徴があります。 ガラスのような質感と透明感 光の反射/屈折のようなリアルな動き 流動性のあるアニメーション https://www.apple.com/jp/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/ より引用 ナビゲーションバーやタブバーなどの標準コンポーネントにデザインが適用されるので既存アプリの確認が必要になります。 課題 WEARアプリの確認と現在の状況を踏まえ、次のような課題がありました。 UIが大きく変わることでデザイン崩れが発生 デザイナー、エンジニア、QAのコスト増大が予想される Liquid Glassの学習コスト UIが大きく変わることでデザイン崩れが発生 タブバーとナビゲーションバーを透過させて確認したところ複数の問題が見つかりました。 タブバー タブバーの裏側にコンテンツが透過しない Before After タブバーとコンポーネントが被る Before After ナビゲーションバー ボタンに枠がつく Before After ナビゲーションバーの下に配置してあるコンポーネントとの一体感を失う Before After その他のコンポーネント Switchが大きくなったことによるレイアウト崩れ Before After アラート(アクションシート)の変化 Before After こちらは一部で、この他にも細かいところの問題は存在しています。 デザイナー、エンジニア、QAのコスト増大が予想される WEARは、基本的に3つのOSをサポートしています。今は過渡期のためiOS 16もサポート中ですが、近々iOS 17, 18, 26のサポートになります。サポートOSは、メジャーバージョンが登場してから数ヶ月後の更新となるため、1年に1回更新されます。そのため、現時点から約2年間は、iOS 26未満をサポートする必要があり、Liquid Glassに対応すると新旧それぞれのデザインを並行管理していく必要が出てきます。それぞれのデザイン調整、開発、テストを行うことで、担当する各チームのコストが増大すると予想されます。 Liquid Glassの学習コスト 新しい概念のため、まずはLiquid Glassを理解する必要があります。既存のコンポーネントのどこをどのような形にすることでLiquid Glassとしてベストなのか見極める必要もあります。まだ登場して間もないため、ノウハウも少なく手探り状態になることは否めません。さらにデザイナーのリソースもこちらに多くを割ける状態ではありませんでした。 移行戦略 リスクと対応コストを考慮し、次の方針で進めます。 Liquid Glassを無効にするオプションをフルで利用する レイアウト修正を最優先で行う レイアウト修正を行いながら原因を把握し、デザインや実装方法を見直す レイアウト修正完了後、Liquid Glassのベストプラクティスを探る Liquid Glassの為に構造を大きく変える変更は可能であればiOS 18以前にも適用させる 1に関してですが、Appleは、Liquid Glassを無効にするオプションを用意しており、現時点では 次のメジャーバージョンリリースまで(約1年)有効 とされています。 開発プロセスの整備 修正後は新旧デザインの確認が必要になるため、効率よく進めるための状態を作る必要があります。 ブランチ戦略 普段WEAR iOSでは、developブランチを除外したGit-flowで開発しています。今回の対応では、Liquid Glass用の開発ブランチを作成することも考えられます。しかし、生存期間は長くなりマージコストの増大が懸念されるため既存と同じように進めます。そのため、旧デザインに影響を与えないように対応していく必要があります。 Xcode 26対応 当たり前ですが、Xcode 26対応は必須です。Xcode 26対応の前に先行して進めることもできますがとても非効率になります。 Xcode 16を使用している場合の例 Xcode 26でビルドが通るブランチを用意 Xcode 26で不具合の修正 Xcode 16用のブランチに切り替え Xcode 16用のブランチに修正を取り込む Xcode 16でビルドし確認 問題があれば2から繰り返し Liquid Glassを無効にする Liquid Glassを無効にする設定です。 Info.plist に UIDesignRequiresCompatibility を追加することで無効にできます。 Liquid Glass効果を動的に切り替えられるようにする こちらを参考に swizzling を使ってRelease版には影響がないよう、デバッグメニューから動的に変えられるようにしています。 zenn.dev デバッグメニューの設定 Liquid Glass対応で分岐できるようにする 同一ブランチでの開発になるため、タブバーの透過等で分岐が必要になります。不具合の修正では極力Liquid Glass用の分岐は入れたくないですが、どうしても必要になるケースも考えられます。また、Liquid Glass無効化オプションの廃止後も、iOS 18をサポートする期間があることを考慮します。 class LiquidGlass : NSObject { @objc static var isEnabled : Bool { UserDefaults.standard.isLiquidGlassEnabled } } WEARには一部Objective-Cが残っているため、Objective-Cからも参照できるようにしています。今はデバッグメニューから切り替えられる値を参照してますが、オプションが無効になってからは次のように変える予定です。 class LiquidGlass : NSObject { @objc static var isEnabled : Bool { if #available(iOS 26.0 , * ) { true } else { false } } } 対応内容を一部紹介 レイアウト修正の中で、最初に取り掛かったのはタブバー周りの対応です。タブバーの背景を透過させるだけでもLiquid Glassらしさが出ます(個人の感想です) Before After WEARのタブバーは背景色が指定されており、タブバーの裏に画面が回り込むような実装になっていない画面が多数ありました。 そういった画面の大多数はスクロールできる画面なので、 ScrollView 系を修正します。 ※WEARでは、SwiftUIの導入も徐々に進めてはいますが、まだまだUIKitがメインなのでUIKitの対応になります。 主に次の2つを利用しているところの修正が必要でした。 contentInsetAdjustmentBehavior = .never extendedLayoutIncludesOpaqueBars = true contentInsetAdjustmentBehavior = .never の対応 iOS 11以前は scrollView.contentInset を実装側で調整する必要がありましたが、iOS 11からは自動で調整してくれるようになりました。当時の選択肢としては、 never or automatic の2択が取れましたが、 WEARでは never を採用していました。その背景もあり scrollView.contentInset.top は contentInsetAdjustmentBehavior = .never を指定して調整していました。 contentInsetAdjustmentBehavior = .never の問題点 contentInset.top だけの設定であればまだ良かったのですが、今回の対応で contentInset.bottom も調整する必要があります。 never のままだと、タブバーの高さをセットする必要があり煩雑になるので、次の方法に変更しました。 contentInsetAdjustmentBehavior = automatic にする(デフォルト値なので指定なし) オートレイアウトの bottom は superview に貼る 注意点は、今まで scrollView.contentInset を参照している場所は scrollView.adjustedContentInset を参照するように変える必要があるところです。 adjustedContentInset は自動調整された後の値になっているので automatic にした際は、こちらの参照が適切になる箇所が出てきます。 extendedLayoutIncludesOpaqueBars = true の対応 こちらは、ナビゲーションバーの裏側に画面を通すために使用しており、 edgesForExtendedLayout = .top とセットで使用されています。タブバーを透過させるため bottom も追加します。 extendedLayoutIncludesOpaqueBars = true edgesForExtendedLayout = [.top, .bottom] ただこれだと旧デザインで、スクロールした最下部のアイテムがタブバーの裏に隠れてしまいます。 最下部のアイテムがタブバーの裏に隠れている そのため、次のようにします。 extendedLayoutIncludesOpaqueBars = true edgesForExtendedLayout = isLiquidGlassEnabled ? [.top, .bottom] : .top 極力分岐はしたくないと上述してましたが、早速使ってしまっています。ただこちらは、Liquid Glassのリリースをする前にタブバーを半透明にする対応をリリースし、事前に分岐を無くしておきたいと考えています。 まとめ 本記事ではWEARアプリにおけるLiquid Glass対応を進めるための取り組みを紹介しました。これからLiquid Glass対応に取り組もうとしている方の参考に少しでもなれば幸いです。まずは不具合を修正し、その後、最適なLiquid Glassの対応を模索していければと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
動画
該当するコンテンツが見つかりませんでした







