iOS - TECH PLAY - TECH PLAY

TECH PLAY

iOS

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

こんにちは、トモニテ開発部 iOS エンジニアの村田です。 iOS エンジニアもしくは Android・クロスプラットフォームを含めてモバイルエンジニアの皆さんに対して気になっていることがあります。 みなさん AI 開発どんな感じでやってますか?どんなハーネス組んでますか? エンジニアリング業界では、単に指示を出して書いてもらう「バイブコーディング」の次の段階として、AI エージェントの自律化や「ハーネスエンジニアリング」「ループエンジニアリング」といった話題が急速に広がっています。 Web やサーバーサイド領域では、こうした最新の AI 活用の情報が活発に共有されている一方、iOS(モバイル)開発における実践的なナレッジはまだまだ各所に散らばっており個々で孤軍奮闘している印象を受けています。 私自身、久しぶりに iOS 開発に取り組んでいて、Claude Code と git worktree を用いた並行開発をする中で iOS 特有のハマりどころをいくつか感じました。 今回は「並行開発」という観点にフォーカスして、iOS で並行開発したときに感じた課題と対処方法を記載します。 開発環境 前提として、以下のような環境・仕組みで開発しています。 使用技術 AIエージェント : Claude Code エディタ : VSCode 並行開発 : Git Worktree 対象 : iOS アプリ(Swift / UIKit / SPM / .xcodeproj) ビルド方法 : XcodeBuildMCP(Claude 経由の MCP)・XcodeBuildMCP(CLI 版)・Xcode(GUI) ディレクトリ構成 <repo>-trees/ ← worktree develop/ ← メイン worktree(base ブランチ) feature-A/ ← タスクごとの worktree(並列) feature-B/ ← タスクごとの worktree(並列) refactor-C/ ← タスクごとの worktree(並列) ... 開発手順 タスク開始時に git worktree add で develop ブランチから新規 worktree を切る worktree 配下で Claude Code のセッションを開始し、要件定義 → 実装 → PR 作成など開発フローを進行 タスクが終わったら、その worktree ごと片付ける ---bin なぜ並行開発が欠かせないのか あちこちで語られている話なので、要点だけ。 AI に開発させると、人間の役割は「実装する人」から「複数のエージェントを監督する人」に変わります。「どこまで自律的に AI に開発させられているか」にも依りますが、設計・実装・ビルド・テストと多くのフェーズで人間の手が空くようになるため、1 タスクを直列で進めるよりも複数タスクを並行で回す方が効率よく進められます。 そこで注目を浴びたのが git worktree です。ブランチごとに独立した作業ディレクトリ(worktree)を持てるため、作業ファイルの競合を気にせず、安全に複数タスクを同時進行できます。 そうして git worktree を用いて iOS 開発を始めたのですが、いくつか課題が発生しました。 問題その1: DerivedData がストレージを食い尽くす 何が起きたか iOS のビルドでは DerivedData を生成します。DerivedData には Xcode がビルドのたびに吐き出す中間生成物(ビルドキャッシュ・インデックス・成果物など)が含まれており、デフォルトでは ~/Library/Developer/Xcode/DerivedData/ に生成されます。XcodeBuildMCP でビルドする場合は、 ~/Library/Developer/XcodeBuildMCP/workspaces/<worktree>-<hash>/DerivedData/<プロジェクト名>-<hash> に生成されます。 DerivedData は worktree の内部ではなく、ユーザー共通の場所にまとめて溜まるため、worktree の数に比例して独立したビルドキャッシュが蓄積されていきます。 私の環境では 1 worktree あたり数 GB〜20 GB 台、合計およそ 86 GB になっていました。 結果としてディスクの空き容量が枯渇し、スワップ領域も確保できなくなって、ビルドどころではなくなりました。 # Xcode(GUI)の既定 — プロジェクト名 + ハッシュ名 ~/Library/Developer/Xcode/DerivedData/ ├── <プロジェクト名>-a1b2c3d4efgh…/ ├── <プロジェクト名>-e5f6g7h8ijkl…/ ├── <プロジェクト名>-i9j0k1l2mnop…/ ├── <プロジェクト名>-q7r8s9t0uvwx…/ └── … # XcodeBuildMCP の既定 — worktree 名 + ハッシュ名 ~/Library/Developer/XcodeBuildMCP/workspaces/ ├── develop-a1b2c3…/DerivedData/ 19 GB ├── feature-A-d4e5f6…/DerivedData/ 21 GB ├── feature-B-g7h8i9…/DerivedData/ 11 GB ├── feature-C-j0k1l2…/DerivedData/ 8.8 GB └── …(他 4 worktree) 26 GB 計 86 GB キャッシュを削除しようと試みたところで Xcode の場合、DerivedData のフォルダ名がハッシュ化されていて、フォルダ名だけではどの worktree に対応するかわからない ビルドしなくてもインデックス更新などで更新日時が変わるため、「更新日が古い=不要」という判断も効かない といった理由により、使い終わった worktree のゴミだけを削除することが難しかったです。 かといって全部消すと、全 worktree がコールドビルドに逆戻りしてしまいます。 XcodeBuildMCP はデフォルトでフォルダ名に worktree 名が入るため対応関係は分かりやすいのですが、 git worktree remove (worktree の削除)をしても、この孤児フォルダが残り続ける点は Xcode の既定と同じです。 対処方針: DerivedData を worktree 配下に保存する 対応策として DerivedData の置き場所を worktree フォルダの直下( <worktree>/DerivedData )に固定すると、次のメリットが得られました。 どのキャッシュがどの worktree のものか、置き場所を見れば分かる git worktree remove すれば DerivedData も一緒に消える 開発時のライフサイクルは以下のようなイメージです。 DerivedData の置き場所を変更する方法 ① Xcode の GUI でビルドする場合 全プロジェクト一律でよい場合 : Xcode → Settings → Locations → Derived Data を Default から Relative に変える 特定のプロジェクトだけ有効にしたい場合 :対象プロジェクトを Xcode で開いた状態でメニューの File → Workspace Settings… ( .xcworkspace を開いていない場合は Project Settings… )を選び、 Derived Data を Workspace-relative Location に切り替える 後者の場合、実態としてはプロジェクト内の WorkspaceSettings.xcsettings ( xcuserdata 配下・通常は Git 管理外のユーザーローカル)の設定と同等のため、GUI を使わず次の内容を直接書いても実現できます。 <? xml version = "1.0" encoding = "UTF-8" ?> <!-- <project>.xcodeproj/project.xcworkspace/xcuserdata/<user>.xcuserdatad/WorkspaceSettings.xcsettings --> <! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" > <plist version = "1.0" > <dict> <key> DerivedDataLocationStyle </key> <string> WorkspaceRelativePath </string> <key> DerivedDataCustomLocation </key> <string> DerivedData </string> </dict> </plist> ② XcodeBuildMCP でビルドする場合 こちらは .xcodebuildmcp/config.yaml の derivedDataPath で決まります(worktree のルートからの相対パスで解決されます)。 # <worktree>/.xcodebuildmcp/config.yaml schemaVersion : 1 sessionDefaults : projectPath : "<プロジェクトパス>" scheme : "<スキーム名>" derivedDataPath : "./DerivedData" platform : "iOS" useLatestOS : false bundleId : "<バンドル ID>" 💡 どちらの方法でも DerivedData がリポジトリ配下に生成されるため、 .gitignore に DerivedData/ を追加する必要があります。 ただし「Xcode 版で特定のプロジェクトだけ Relative にしている場合」や「XcodeBuildMCP 版で .xcodebuildmcp/config.yaml を Git 管理していない場合」は、新しい worktree を作るたびにこれらの設定を行う必要があります。毎回手動で設定するのは面倒なので、Git の post-checkout hook で worktree の作成時に自動設定されるようにするのがおすすめです。 post-checkout hook で DerivedData の設定を自動化する git worktree add や git checkout を実行すると、 post-checkout という hook が発火します。hook は共通の Git ディレクトリ( git rev-parse --git-common-dir )に置かれ全 worktree で共有されるので、そこに設定することで新規 worktree 作成時の処理を仕込むことができます。 下記のような処理を .git/hooks/post-checkout に記述することで、新規 worktree 作成時に「DerivedData を各 worktree 配下( <worktree>/DerivedData )に保存する」ように設定できます。 なお以下の hook では ② の config.yaml を直接出力していますが、正の config.yaml をデフォルトブランチなどで管理し、post-checkout で cp する方式でも構いません。 #!/bin/bash # .git/hooks/post-checkout if [ " $3 " = "1" ]; then PROJECT = $( find . -maxdepth 1 -name " *.xcodeproj " | head -1 ) if [ -n " $PROJECT " ]; then # ① Xcode GUI 用: WorkspaceSettings に worktree 相対の DerivedData を書く DIR = " ${PROJECT} /project.xcworkspace/xcuserdata/ $( whoami ) .xcuserdatad " PLIST = " ${DIR} /WorkspaceSettings.xcsettings " mkdir -p " $DIR " [ ! -f " $PLIST " ] && plutil -create xml1 " $PLIST " plutil -replace DerivedDataLocationStyle -string WorkspaceRelativePath " $PLIST " plutil -replace DerivedDataCustomLocation -string DerivedData " $PLIST " # ② XcodeBuildMCP 用: config.yaml を worktree 直下に生成 if [ ! -f .xcodebuildmcp/config.yaml ]; then mkdir -p .xcodebuildmcp cat > .xcodebuildmcp/config.yaml <<'YAML' schemaVersion: 1 sessionDefaults: projectPath: "<プロジェクトパス>" scheme: "<スキーム名>" derivedDataPath: "./DerivedData" platform: "iOS" useLatestOS: false bundleId: "<バンドル ID>" YAML fi fi fi 問題その2: シミュレータを複数セッションが奪い合う 何が起きたか iOS 開発では、シミュレータ上での実操作が必要な場面が多々あります。 画面情報・遷移情報の取得 コード変更後の動作確認 E2E テストの実行 AI に並行開発させている中でこれらの動作をさせようとすると、1 台のシミュレータを複数のセッション(エージェント)が奪い合い、開発が滞る問題が発生しました。 操作の割り込み・競合: あるエージェントの検証中に、別のエージェントが起動・操作を行って画面を奪い合う 状態の破壊: 実行中のアプリ領域やデータが上書きされ、E2E テストや動作確認が誤判定される 処理の順番待ち: シミュレータの空き待ちが発生し、並行開発のスピード感が失われる 対処方針: worktree ごとに専用シミュレータを持たせる 対策として、 worktree ごとに専用シミュレータを持たせる運用にしました。 具体的にはシミュレータ名を <worktree 名> - <元の機種名> (例: feature-A - iPhone 16 Pro )のように紐付けます。これにより「このセッションが触っていいのはこの 1 台のみ」と明確にし、他のセッションで稼働中のシミュレータへの誤干渉を防いでいます。タスクが完了して役目を終えたシミュレータはシャットダウンし、別の worktree での開発で名前を変更して再利用する流れです。 💡 基本的には AI による並行開発を前提としていますが、人間が最終的な動作確認を行う場合にもメリットがあります。各 worktree 専用のシミュレータ上にビルド済みアプリがそのまま残っているため、スムーズに動作確認を進められます。 シミュレータの選定ルール ある worktree の開発で使うシミュレータの選定ルールは、以下のようにしました。 worktree 名で始まるシミュレータが起動済みなら、それをそのまま使う worktree 名で始まるシミュレータが未起動なら、起動して使う 無ければ、起動していないシミュレータを <worktree 名> - <元の機種名> にリネームして起動する これを実装に落とし込んだものが以下のコードです。 resolve_udid は ①→③ の順に「その条件に合うシミュレータがあるか」を確認していき、最初に見つかった 1 台を、その worktree に対応するシミュレータの UDID(デバイスの一意 ID)として採用します(見つかった時点で残りは確認しません)。あとはこの UDID を指定してビルドすれば、その worktree 専用のシミュレータに向けて実行できます。 wt = " $( basename " $PWD " ) " # worktree 名(例: feature-A)をシミュレータ名の接頭辞に使う # シミュレータ一覧を JSON で取得して devices_json にキャッシュする refresh_devices() { devices_json = $( xcrun simctl list devices available --json ) ; } # 未起動のシミュレータを起動する boot_sim() { xcrun simctl boot " $1 " 2 > /dev/null ; } # UDID から元の機種名を引く(既に worktree 名が付いていれば落とす。付け足すと再利用のたびに接頭辞が積み重なるため) sim_name() { printf ' %s ' " $devices_json " | jq -r --arg u " $1 " \ ' [.devices[][] | select(.udid == $u) | .name][0] // empty | sub("^.+ - "; "") ' } # 指定した起動状態(booted / unbooted)で、worktree 名で始まるシミュレータの UDID を返す(無ければ空文字) pick_named() { printf ' %s ' " $devices_json " | jq -r --arg wt " $wt " --arg want " $1 " \ ' [.devices[][] | select((.name | startswith($wt + " - ")) and (if $want == "booted" then .state == "Booted" else .state != "Booted" end)) | .udid][0] // empty ' } # 未起動(Shutdown)の空きシミュレータを1台返す(無ければ空文字) pick_spare() { printf ' %s ' " $devices_json " | jq -r \ ' [.devices[][] | select(.state == "Shutdown") | .udid][0] // empty ' } resolve_udid() { refresh_devices # ① worktree 名で始まるシミュレータが起動済みなら、そのまま使う udid = $( pick_named booted ) ; [ -n " $udid " ] && { echo " $udid "; return; } # ② worktree 名で始まるシミュレータが未起動なら、起動して使う udid = $( pick_named unbooted ) ; [ -n " $udid " ] && { boot_sim " $udid "; echo " $udid "; return; } # ③ 無ければ、空きシミュレータの接頭辞を「<worktree 名> - 」に付け替えて起動する spare = $( pick_spare ) [ -z " $spare " ] && { echo " 空きシミュレータがありません " >&2; return 1 ; } xcrun simctl rename " $spare " " $wt - $( sim_name " $spare " ) " boot_sim " $spare " echo " $spare " } 開発フローとクロージング処理 ここまでの処理を開発フローに沿って並べると次のようになります。 開始 : タスクごとに worktree を切り、専用の作業場を用意する 実装 : その worktree で Claude セッションを開始し、1 つの機能を実装する 動作確認 : その worktree 専用のシミュレータを起動し、ビルド & 実行・テストを行う レビュー : 実装が固まったら PR を作成する クロージング処理 : PR マージと同時に、worktree 削除・Issue クローズ・シミュレータのシャットダウンを行う 開発フローの最後には、クロージング処理を置いています。 close-feature のようなスキルにまとめ、PR マージ → Issue クローズ → worktree 削除 → 専用シミュレータのシャットダウンを一括で実行しています。 このクロージング処理をフローに組み込むことで、問題その1で挙げた DerivedData によるストレージ圧迫を防ぎつつ(worktree を削除すれば配下の DerivedData も一緒に消える)、起動したままのシミュレータが積み上がってメモリを圧迫するのも同時に抑えられます(シャットダウンした端末は、次のタスクで再利用される)。 問題その3: .xcodeproj のコンフリクトが多発する .xcodeproj を Git 管理しているとブランチの切り替えやマージのたびにコンフリクトが起きやすい、という問題があります。これ自体は昔からある iOS 開発の悩みですが、AI に複数タスクを並行開発させるようになってコンフリクトの頻度も増え、無視できないコストになったと感じています。 トモニテの PJ では実現できていませんが、下記の理由により XcodeGen を用いた YAML 管理への移行を検討しています。 1. 並行開発でのコンフリクト軽減 git worktree などを活用して複数ブランチでの並行開発を進める場合、 project.pbxproj のコンフリクトが多発する可能性があります。 特に AI の活用によって開発の速度や並行性が上がるほど、その発生頻度も多くなりがちです。 XcodeGen を導入して .xcodeproj を自動生成の成果物として扱い .gitignore に追加することで、 .xcodeproj でのコンフリクト問題を解消できると考えています。 2. AI と YAML の相性 Xcode のプロジェクト管理は GUI 操作を前提とした仕組みであり、AI エージェントとは相性が良くありません。これを project.yml による宣言的なテキスト管理に切り替えることで、「YAML での管理・変更 → コマンド実行( xcodegen generate )」という AI に適したフローで構成を変更できるようになると考えています。 3. コードレビューのしやすさ コード差分が複雑な pbxproj からシンプルな project.yml に変わることで、変更内容を容易に把握できます。 AI がコードを書き、人間がレビューするフェーズでは可読性の観点でもメリットがあると考えています。 まとめと残課題 ここまで git worktree を用いた iOS の並行開発で出会った問題点と、その対処方法を紹介してきました。 とはいえ、こうして並行タスクを回す仕組みを整えても、human-in-the-loop 的な体制では並行開発の効果も限界があると感じています。監督する人間のコンテキストスイッチがボトルネックになるため私の場合、同時に見られるのは 3〜4 タスクほどでした。これでは開発生産性も頭打ちになり、「プロダクトや事業をどう伸ばすか」という本質的な問いに集中できないと感じています。 この課題を突破するためには、やはり「ループエンジニアリング」など AI がより自律的に開発を回せるような仕組みを追求していくことが不可欠だと感じています。 関連リンク XcodeBuildMCP XcodeGen
はじめに ZOZOTOWN開発本部のらぷ( @laprasdrum )とイッセー( @15531b )です。WWDC現地参加は、らぷは2016年以来2回目、イッセーは初参加(かつ初渡米)です。本記事ではWWDC26の現地参加レポートとともに、ZOZOのiOSエンジニアによるおすすめセッション、6月18日に開催されたLINEヤフー株式会社との合同報告会イベントについてもご紹介します。 目次 はじめに 目次 WWDC26のSpecial Event Day 0 Day 1 Keynote Download stations Platforms State of the Union 現地視聴をより楽しむために In-person labs Inner ring reception Day 2 Developer session Mixer @Developer Center Theater event 併催コミュニティイベント おすすめセッション集 優れたデザインのための原則 Foundation Modelsフレームワークの新機能 WWDC26 報告会 at LINEヤフー, ZOZO さいごに WWDC26のSpecial Event 今年6月7日〜9日(現地時間)のWWDC26では、昨年と同様に招待制のSpecial Eventが現地開催されました。昨年の様子は「WWDC25現地参加レポート」をご覧ください。 techblog.zozo.com 今年のSpecial EventはWelcome receptionから始まりました。基調講演後には、Appleの方へ新APIを質問したりアプリのフィードバックをもらえたりするIn-person labsが開かれました。その後、Apple ParkのInner ringで交流会がありました。最終日はSteve Jobs Theaterで、スター・ウォーズ作品『The Mandalorian and Grogu』の上映とスペシャルゲストのインタビューを楽しめました。 Date Pacific Time Content Venue Day 0 (June 7) 3 p.m. Welcome reception Infinite Loop Day 1 (June 8) 8 a.m. Check-in Apple Park 10 a.m. Keynote Apple Park After Keynote Download stations open Apple Park 12 p.m. Lunch Apple Park 1 p.m. Platforms State of the Union Apple Park After Platforms State of the Union In-person labs Apple Park 4-6 p.m. Reception in the inner ring Apple Park Day 2 (June 9) 10 a.m. Developer session Steve Jobs Theater or Apple Developer Center Cupertino 11:30 a.m. Mixer Apple Developer Center Cupertino 8 p.m. Pre-show presentation and special screening of The Mandalorian and Grogu Steve Jobs Theater Day 0 私たちはDay 0の午前にSFO(サンフランシスコ国際空港)へ到着し、宿泊先のホテルに荷物を置いてからInfinite Loopへ向かいました。道中は初めてWaymoを利用しました。急カーブでもほとんど揺れずに走行するため、仮眠できるほど静かでした。 Infinite Loopに到着するとチェックイン待ちの長蛇の列がすでにできていました。並んでいる間のおもてなしとして配られたジェラートバーとお水で暑さをしのぎつつ待ちました。 2種類の味から選べたジェラートバー 入口前でセキュリティチェックを済ませると、参加者用のバッジとノベルティを受け取りました。バッジにはNFCタグが入っており、各イベントのチェックイン時にAppleのスタッフのiPhoneにバッジをかざすことになっています。ノベルティ恒例のピンバッジにはLil Finder Guyと創立50周年を象徴するデザインが含まれていました。入場時はAppleのスタッフの方々が歓声とともに出迎えてくださり、Day 0から盛り上がりを肌で感じました。 今年のノベルティバッグ 今年のピンバッジ、ステッカー、ボトル 参加者用バッジ ── お気に入りのピンバッジを添えて その後は終了時刻の夕方7時まで自由に過ごしました。軽食とドリンクをいただきながら各国の参加者との交流、Apple Design Awardsの受賞者とファイナリストとのトーク、Appleのスタッフによるモニュメント前の記念撮影などを楽しみました。 快晴に恵まれたこの日 モニュメント前で記念撮影 また、APACの参加者で集まって記念写真も撮りました。オフラインで参加すると、多くの方がApple Platformの開発者として関わっていることを実感できました。 Day 1 Keynote まずはKeynoteの会場に向かうため、Visitor Center前でチェックインを済ませてApple Parkに入場しました。入場まで横断歩道の信号待ちで列がゆっくり進む中、Caffè Macsのスタッフからいただいたドーナツを食べながら近くの方と「今年のWWDCは何を期待してますか?」と雑談しました。話しかけた方の多くがAIのトピックを気にされていました。 Visitor Center前のチェックインも長蛇の列 Caffè Macsからいただいたオレンジ味のドーナツ 会場に到着したのは8時半頃でした。Keynote開始時間の10時までは自由に散策し、参加者とお話を楽しみました。 KeynoteおよびPlatforms State of the Unionの会場 会場のスクリーンでは、オンラインで公開されているKeynoteの動画が再生されます。現地では動画の再生前にCraig Federighi氏がステージに登場し、挨拶がありました。 developer.apple.com 続いてTim Cook氏も登場し、このときの会場の盛り上がりは強烈でした。CEOとしてのKeynoteでの登壇は今年が最後です。参加者全員が立ち上がってiPhoneを掲げて撮影する光景を前に、Cook氏自身も「これまでこんなにiPhoneに囲まれたことはありません」と語っていました。 壇上に登場したTim Cook氏 Download stations Keynoteが終了すると、高速の有線ネットワークが提供されるDownload stationsに集まりました。ここでXcode 27 BetaやiOS 27 Betaのダウンロードを始めました。この日は晴天で気温も高かったため、パラソルの影に集まって休憩する参加者も多かったです。 有線ネットワークが用意されたDownload stations Platforms State of the Union developer.apple.com 手元にiOS 27 BetaをインストールしたiPhoneを触りながら、先のKeynoteの内容も踏まえてPlatforms State of the Unionを視聴しました。今回のSiri AIやCore AI、Foundation Modelsなどのアップデートには、共通した方向性があります。デバイスへ蓄積された写真やテキストなどのパーソナルなデータをもとに、AI体験を最適化することがAppleの狙いです。Newsroomで公開されたSiri AIの記事には、Apple Intelligenceのアーキテクチャ図が掲載されています。この図では、デバイスとアプリケーション(またはSiri AI)の間にユーザーコンテキストが示されています。Appleプラットフォームのアプリ開発者には、このコンテキストを活かしたAI活用が求められているのでしょう。 www.apple.com 視聴後しばらくすると、スクリーン上にさまざまなアプリのアイコンが表示されました。幸運なことに弊社のアプリが映し出されたタイミングで撮影できました。 どこかにある弊社アプリのアイコン。見つけられましたか? 現地視聴をより楽しむために 現地ならではの視聴の楽しみ方も、二人それぞれ工夫してみました。らぷは Rokid Glasses というスマートグラスのリアルタイム翻訳機能を使って視聴しました。体感1秒未満で翻訳結果が表示されたので和訳文も追いつつ快適に視聴できました。 Rokid Glassesのリアルタイム翻訳結果 イッセーは英語に自信がなかったため、セッションの視聴でAirPods Proのライブ翻訳機能を試しました。実際に使ってみると、登壇者の英語が純正の翻訳アプリ上に日本語テキストとして表示されるだけでなく、AirPodsからも日本語に翻訳された音声がリアルタイムで流れてきます。この機能のおかげで、英語に自信がなくてもセッションの内容を大まかに理解できました。イッセーとしては、画面上のテキスト翻訳はあえて見ず、登壇者の様子を見ながら日本語音声だけを聞くスタイルがとても快適でした。 ライブ翻訳機能の翻訳結果 私たち以外にもスマートグラスやAirPodsを使って視聴している参加者が多くいました。海外カンファレンスへ現地参加する際の言語の壁はこれまでもありましたが、こうした技術によって乗り越えやすくなったと感じました。 In-person labs Platforms State of the Unionが終了するとIn-person labsが始まり、各ラボで新しく発表されたAPIや日々の開発で困っていることを相談しに行きました。特に困ったことがなくても「まだ触れたことのないFrameworkを始めようと思うんだけど何から始めたら良いですか?」といった相談でも問題ありませんでした。気さくな雰囲気の中でいろいろ話せるので、来年現地に行かれる方はぜひ積極的に利用してみてください。 Inner ring reception その後夕方6時まではInner ring内で自由に過ごしました。Xcode 27 Betaで開発したり、初対面や既知の開発者との会話を楽しんだり、Appleの方に質問したりと、あっという間に時間は過ぎていきました。 Xcodeダウンロード中にAppleのWWDRの方にいただいたFoundation Models Frameworkのステッカー 虹のApple Stage前で撮影してもらったイッセー。青いシャツの方は各国のApple Storeのスタッフ Day 2 Developer session Developer sessionに参加するため、Apple Parkへ向かいチェックインを済ませました。今回のセッションはSteve Jobs Theaterで開催されました。ここは過去のWWDCでは立ち入ることができなかった特別な場所です。定員制のため、シアターに入れない場合はApple Developer Centerでの視聴でした。開始1時間前に到着したものの、すでに多くの参加者が列を作っていました。皆がこのシアターでの参加を待ち望んでいる熱気を感じました。 無事に入場でき、ガラス張りの円形の建物と緑豊かな造形美に圧倒されました。会場で軽い朝食をとった後、いよいよセッションがスタートしました。 Developer session前のSteve Jobs Theaterの様子 今回のセッションでは、Xcode 27の「Agentic Coding」やFoundation Modelsなどの新しいAIフレームワークが取り上げられました。実際のアプリ開発にどう組み込むかという実践に焦点を当てた内容です。その中でも、Evaluations Frameworkはどのように役立てられるのかイメージができていませんでした。セッションを通して、LLMの出力の品質を容易にテスト・評価できる点を理解でき、実際に触ってみる良いきっかけになりました。動画でも公開されているので、使い方のイメージが付いていない方の参考になります。ぜひチェックしてみてください。 developer.apple.com Developer sessionの様子 Mixer @Developer Center セッション終了後はDeveloper Centerへ移動し、「Mixer」というイベントに参加しました。会場にはさまざまなフードトラックが用意されており、昼食をとりながらコーディングをする人々の姿も見られました。また、特定のテーマごとにブースが設けられており、参加者同士で交流したり、Appleのエンジニアに直接質問したりできる貴重な場となっていました。さらに、Developer Center内ではGroup Labが開催されており、会場の内外いずれも非常に活気に満ちていました。 テーマごとに交流ができる場所 Mixerの会場図 Theater event 夜は、現地参加者の中で先着申し込みができた「Theater event」に参加しました。日中と同じくSteve Jobs Theaterで開催されるとのことで、再びあの空間に足を踏み入れることができました。 Theater eventでのSteve Jobs Theater 今回上映されたのは、今年公開されたスター・ウォーズ作品『The Mandalorian and Grogu』です。上映前にはスペシャルゲストによる対談が行われていました。その際、通路の透明なディスプレイに会話の文字起こしと手話がリアルタイムで表示されていました。Appleらしいアクセシビリティへの高い配慮に、とても驚かされました。スター・ウォーズにあまり精通していなかったため少し心配でしたが、思った以上に内容が分かりやすく、シーンによっては観客の歓声で盛り上がるなど、非常に楽しむことができました。 インタビュー中に字幕と手話が表示される透明なディスプレイ 併催コミュニティイベント らぷはCommunityKitが主催している2つのイベントに参加してきました。ひとつはPaul Hudson氏が開催した「What's new in iOS 27?」です。 luma.com WWDCでは、Day 1にAppleから全セッション動画とサンプルプロジェクトが公開されます。Hudson氏はSwiftUIやSwiftDataなどのサンプルプロジェクトを実際にビルドし、発生したエラーと修正方法を紹介してくださいました。すべてのサンプルプロジェクトが修正なしで期待どおりに動作するわけではないので、こうした知見を聞けるのは助かります。 会場の様子 もうひとつは「Swift Contributor Social」というイベントです。こちらはSwiftのコントリビューターと、それに興味がある人たちによる交流会です。まだコントリビューターではありませんが、どんな観点で活動しているのか聞いてみたくて参加しました。 luma.com 今回はstdlibのメンテナーでSpanの実装を担当された方にじっくりお話を聞くことができました。 developer.apple.com メモリ安全性への興味や今後の展望、コンパイラーチームが別途持っている構想と突き合わせた上での定期的な議論などを伺いました。Swift Forumsを眺めているだけでは得られない情報が多く、刺激的な時間でした。 ボウリング場のビリヤード台を囲んで自由に交流中 参加中、もしくは興味のあるWorking Groupのバッジ おすすめセッション集 優れたデザインのための原則 ZOZOTOWN iOSアプリの開発をしているつっきー( @tsuzuki817 )です。 今回のWWDCでは、Appleが考える優れたデザインの原則を扱うセッションがありました。デザインを単なる「見た目やふるまい」ではなく、「意図をもってものを作ること」と定義した上で、数々の重要な指針を解説しています。詳細は「 Principles of great design 」のセッションをご覧ください。 私が特に注目したのは、「主導権」「寛容さ」「柔軟性」という3つの原則のつながりです。ユーザーをあらかじめ決められた道に無理に誘導するのではなく、主導権を人々に委ね、自分のペースで探索させることの重要性が語られています。しかし、自由に探索できるようになると、ユーザーは誤って削除してしまったり意図しない操作をしてしまったりすることがあります。 そこで重要になるのが「寛容さ」です。操作を簡単に取り消せるようにし、大惨事を避けられるようにすることで、ユーザーに「いつでも回復できる」という自信を与え、安全な探索を支えることができます。さらに、ユーザーがアプリを使う状況(ランニング中や運転中など)や能力は人それぞれであるため、それらに適応する「柔軟性」を持たせることも強調されていました。 すべての人に合う単一のレイアウトを見つけるのは難しいため、ユーザー自身が好みに合わせて体験をカスタマイズできるようにすることが、最良の柔軟性です。セッションの最後では、これらの原則を正しく実行した自然な結果として、真の感情的なつながりである「喜び」が生まれると語られていました。これは製品に込めた「思いやりの総和」であるとのことです。自分がアプリを開発するうえで大事にしていることでもあり、同時に難しいと感じている点でもあります。 普段ZOZOTOWNの開発や個人開発をするなかで、ユーザーにどのような体験をさせたいのか迷うことが多くあります。このような原則に則って考えることで、よりよいプロダクトが作れそうだと期待しました。また、この考え方はZOZOTOWNのアプリ開発でぜひ意識していきたいです。さらにユーザーだけではなく、普段の仕事で起きるコミュニケーションにも活かせます。相手に選択肢を委ね、ミスを許容し、状況に柔軟に対応していきます。 Foundation Modelsフレームワークの新機能 ZOZOTOWN iOSアプリの開発をしているpe( @shumpei_nagata )です。 今年のWWDCでは、Foundation Modelsフレームワークの進化に興味を惹かれました。詳細は「 What's new in the Foundation Models framework 」で紹介されています。 このフレームワークは昨年登場し、アプリ上でオンデバイスの大規模言語モデルを扱えるようになりました。そして今年は画像入力(Vision)に対応しました。テキストだけだったプロンプトに Attachment(UIImage(...)) のように画像を差し込むと、その画像について回答が得られます。ガイド付き生成( @Generable )やツール呼び出しなどの既存APIもそのまま使えるので、これまで書いたコードを活かして画像対応に広げられます。 また、Foundation Modelsの導入に役立つ道具も増えてきました。生成AIは出力が確率論的に決まるので、生成結果の品質を確かめるのはなかなか骨が折れます。サービスの機能として安心して導入するには、品質を測定できる仕組みが必要です。そういった課題には Evaluations Framework が役立ちます。併せてAppleからオープンソースで公開された foundation-models-utilities も参考になります。コンテキスト超過を防ぐトランスクリプトの圧縮やSkills APIなど、Foundation Modelsをより便利に扱えるツールが揃っています。 このセッションが気になったのは、業務と個人開発のどちらでも視覚的な情報を多く取り扱うアプリを開発しているからです。進化したFoundation Modelsフレームワークを組み込むことで、アプリ上の体験を大幅に拡張できそうです。たとえば説明文の作成、属性の抽出、altテキストによるアクセシビリティの補完などが挙げられます。こうした処理が通信なし・無料で、しかもプライバシーを保ったまま端末の中で完結するのは非常に魅力的です。 とはいえ、既存のサービスへ導入するハードルは低くありません。Foundation Modelsは利用にiOS 26以降が必要で、今回の画像入力はiOS 27以降でないと動きません。その上、Apple Intelligence対応デバイスという条件もつきます。またZOZOTOWNはiOSアプリだけでなく、AndroidアプリやWebサイトなどさまざまなプラットフォームでお客様にご利用いただいています。OSや端末が限られる機能をそのまま主役に据えると、体験に大きな差が生じてしまうので、導入の見極めが難しい技術であるのも事実です。 それでも、アプリに導入するとどのような新しい体験を提供できるかとワクワクしました。技術的な好奇心もくすぐられるセッションでした! WWDC26 報告会 at LINEヤフー, ZOZO WWDC後の6月18日にLINEヤフーと合同で報告会を実施しました。 lycorptech-jp.connpass.com イッセーは「現地で盛り上がったWWDC26 Keynote」というタイトルで登壇しました。 speakerdeck.com 報告会では、実際に現地参加して見えたことを紹介しました。Keynoteで周囲のデベロッパーがどのようなテーマで盛り上がったのか、意外と反応が落ち着いていたテーマはどれか、思いもよらないサプライズは何だったのかを共有しています。一番印象的だったのは、事前に「これが目玉だろう」と考えていたトピックと、会場が実際に沸いたトピックのズレです。この差は公式の発表資料やニュース記事を後から追っても分かりません。世界中のデベロッパーがどのトピックに興味関心があったのかは、その場にいた人にしかわからない情報でした。 らぷはパネルディスカッションに参加し、印象的だったセッションや現地参加に必要な準備などを共有しました。先ほどのスマートグラスの活用や気になったセッションの話も取り上げています。「WWDCに現地参加するなら何をすべきですか?」という質問への回答は、他の参加者やAppleのスタッフの方と積極的にコミュニケーションすることです。これはLINEヤフーのパネラーの方と同じ意見でした。自分にはない考え方に触れたり、同じ考えをもつ同志に出会えたりできる機会です。ぜひ最大限に活用してください。 パネルディスカッションの様子 さいごに 今年のWWDCで感じた現地の雰囲気とおすすめセッション、報告会の様子をお届けしました。この記事を読んで現地での思い出を振り返ったり、新しいAPIの調査を始めて共有したりしていただけると嬉しいです。 ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。WEAR部iOSブロックの坂倉です。 WEARは先日、 SceneDelegate へ移行しました。ライフサイクルそのものに関わる変更のため、画面数の多い歴史あるアプリではなかなか骨が折れました。 この記事では、 WEARが直面した課題 、 課題を解決するために取ったアプローチ 、そして 移行中につまずいた箇所と対処 をご紹介します。 WEARのような大規模アプリが、影響範囲の広い SceneDelegate 対応をどのように進めたのか、その実例として参考になれば幸いです。 目次 はじめに 目次 SceneDelegate対応で直面した課題 iOS 27以降、未対応のアプリは起動できなくなる アプリのライフサイクルに関わる変更のため、すべての画面に影響する ライフサイクルイベントがAppDelegateとSceneDelegateに分かれて呼ばれる すべての画面で問題が出ないか、慎重に調査しなければならない 課題を解決するためのアプローチ アプローチ1. AppDelegateとSceneDelegateの役割を理解する AppDelegateの役割 SceneDelegateの役割 役割と違いのまとめ どの処理をSceneDelegateへ移すか プッシュ通知はAppDelegateに残す アプローチ2. AIを使って影響範囲を特定し、QA観点をまとめる 1. AIを使って影響範囲を把握する 2. AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す 3. AIにQA観点を書き出させる これから対応するならXcode 27の併用も検討を アプローチ3. リスクを抑えるため、最小限の変更にとどめる つまずいた箇所と対処 一番最初のウィンドウを取得するのは危ない 外部ソースからの起動判定はconnectionOptionsで行う さいごに SceneDelegate対応で直面した課題 WEARが直面した課題は大きく2つあります。「対応しないという選択肢がない」ことと、「対応がアプリの全画面に影響する」ことです。 iOS 27以降、未対応のアプリは起動できなくなる Appleは公式ドキュメント Transitioning to the UIKit scene-based life cycle で、 UIScene ライフサイクルの採用が必須になると明記しています。最新SDKでビルドするアプリが対象となり、iOS 27・iPadOS 27・Mac Catalyst 27・tvOS 27・visionOS 27以降、未対応のアプリは起動できなくなります。 つまり、対応しない限り最新SDKでのアップデートを続けられなくなります。 アプリのライフサイクルに関わる変更のため、すべての画面に影響する SceneDelegate に対応すると、これまで AppDelegate が受け取っていたUI関連のライフサイクルイベントは SceneDelegate が受け取るようになります。ライフサイクルはアプリの根幹であり、この変更はWEARが持つすべての画面に影響し得ます。 具体的には次のような難しさがありました。 ライフサイクルイベントがAppDelegateとSceneDelegateに分かれて呼ばれる まず大変だったのが、これまで AppDelegate だけに届いていたライフサイクルイベントが、 AppDelegate と SceneDelegate の2つに分かれて呼ばれるようになる点です。 AppDelegate 中心の頃、起動時のイベントは次の順番で、すべて AppDelegate に届いていました。 application(_:willFinishLaunchingWithOptions:) application(_:didFinishLaunchingWithOptions:) applicationDidBecomeActive(_:) 初期化処理も1つのクラスに集まっているため、実行順序を追うのは難しくありませんでした。 ところが SceneDelegate を採用すると、アプリ起動時の実行順序は次のように2つのクラスをまたぐ形になります。 application(_:willFinishLaunchingWithOptions:) (AppDelegate) application(_:didFinishLaunchingWithOptions:) (AppDelegate) scene(_:willConnectTo:options:) (SceneDelegate) sceneWillEnterForeground(_:) (SceneDelegate) sceneDidBecomeActive(_:) (SceneDelegate) 起動処理が2つのクラスに分散すると、「この初期化はあの処理より先に終わっているはず」という前提が崩れます。たとえば、 SceneDelegate へ移したUIのセットアップが AppDelegate 側の初期化に依存していると破綻します。 さらに、呼ばれるタイミング自体が変わるイベントもあります。 sceneWillEnterForeground(_:) は applicationWillEnterForeground(_:) と違い、アプリ未起動(Not Running)からの起動でも呼ばれます。フォアグラウンド復帰時の処理をそのまま移すと、起動時にも実行されてしまいます。 どの処理を、どちらのクラスの、どのイベントに置くべきか 。アプリ全体の初期化処理を1つずつ見極める必要がありました。 参考: About the app launch sequence すべての画面で問題が出ないか、慎重に調査しなければならない WEARには通常起動のほかに、URLスキーム・ユニバーサルリンク・プッシュ通知など複数の起動導線もあります。特定の条件が揃ったときだけ顕在化するリグレッションもあり得るため、すべての画面について「移行しても問題が出ないか」を1つずつ調査していく必要がありました。 画面数の多いWEARにとって、これは人力だけでやり切れる規模ではありません。 課題を解決するためのアプローチ これらの課題に対して、WEARでは次の3つのアプローチで移行を進めました。 AppDelegateとSceneDelegateの役割を理解する AIを使って影響範囲を特定し、QA観点をまとめる リスクを抑えるため、最小限の変更にとどめる アプローチ1. AppDelegateとSceneDelegateの役割を理解する まず取り組んだのは、Appleの公式ドキュメントを読み込み、 AppDelegate と SceneDelegate がそれぞれ何を担当しているのかを正確に理解することでした。 AppDelegateの役割 UIApplicationDelegate プロトコルに準拠する AppDelegate は、アプリ全体のライフサイクルイベントを管理します。 アプリレベルのイベント管理 :アプリの起動・終了、システムレベルのイベントなど、アプリプロセス全体のライフサイクルを担当する。 application(_:didFinishLaunchingWithOptions:) などを通じて、アプリの初期設定や起動準備をする 初期の環境設定 :アプリ全体のデータやリソースの初期設定、プッシュ通知の処理などを担当する シングルトン :1つのアプリにつき、 UIApplication は1つしか存在しない 注意したいのは、 SceneDelegate に対応すると、UIに関するライフサイクルイベントは SceneDelegate の担当に移り、 AppDelegate には届かなくなるという点です。 SceneDelegateの役割 UIWindowSceneDelegate プロトコルに準拠する SceneDelegate は、「シーン」のライフサイクルイベントに応答します。シーンとは画面に表示されるアプリのUIのひとまとまりのことで、たとえばiPadで同じアプリを2つのウィンドウで並べて開くと、それぞれが独立したシーンになります。 シーンの管理 :アプリのUIを「シーン」として表現し、それぞれのシーンが独自のライフサイクルと状態を持てる。これにより、複数のウィンドウを並べて表示するマルチウィンドウ環境が可能になる シーン固有のライフサイクルイベント :これまで AppDelegate が扱っていたUI関連のライフサイクルイベントを SceneDelegate が担当する 役割と違いのまとめ 特徴 AppDelegate SceneDelegate 責任範囲 アプリ全体のプロセスとライフサイクル(起動・終了・システムレベルイベント)を管理 シーンのライフサイクルを管理 UIの管理 従来はアプリで唯一のUIとウィンドウを管理。iOS 13以降はUI管理の責任が軽減された 各シーンのUIのセットアップ・ウィンドウ管理・UIライフサイクルイベントを処理 マルチウィンドウサポート 単一のウィンドウのみ 複数のウィンドウをサポート 要するに、 AppDelegate はアプリの「プロセス」レベルの管理を担い続けます。一方の SceneDelegate は、シーンごとのUIの管理と、ライフサイクルイベントへの応答を担当します。 どの処理をSceneDelegateへ移すか 役割が分かれば、対応方針は明確です。 AppDelegate で処理していた次の処理を SceneDelegate に移行しました。 説明 移行前(AppDelegate) 移行後(SceneDelegate) 具体例 アプリ起動時のWindow生成処理 application(_:didFinishLaunchingWithOptions:) scene(_:willConnectTo:options:) Windowの生成、起動に必要なデータ取得、GA送信のための設定、URLスキームの受け取り シーンがアクティブになったときの処理 applicationDidBecomeActive(_:) sceneDidBecomeActive(_:) プッシュ通知バッジのリセット シーンが非アクティブになる直前の処理 applicationWillResignActive(_:) sceneWillResignActive(_:) — シーンがフォアグラウンドに入る時の処理 applicationWillEnterForeground(_:) sceneWillEnterForeground(_:) 必要に応じたリロードとAPI実行 シーンがバックグラウンドに入る時の処理 applicationDidEnterBackground(_:) sceneDidEnterBackground(_:) バックグラウンド移行時のUI後処理 ユニバーサルリンク処理 application(_:continue:restorationHandler:) scene(_:continue:) ユニバーサルリンクの受け取り URLスキーム処理 application(_:open:options:) scene(_:openURLContexts:) URLスキームの受け取り プッシュ通知はAppDelegateに残す ここで1つ注意点です。 プッシュ通知はアプリ全体に関わるイベントなので、引き続き AppDelegate で処理します 。 Appleのドキュメント にもある通り、デバイストークンの登録や通知の受信といったプッシュ通知のハンドリングには、 SceneDelegate 側に対応する移行先が用意されていません。 なお、通知への応答がシーンと無関係というわけではありません。通知へのユーザーの応答(タップなどの操作)をシステムがどのシーンに反映するかを示す UNNotificationResponse.targetScene というプロパティが用意されています。通知タップ後の処理でシーンが必要な場合は、このプロパティから取得できます。 アプローチ2. AIを使って影響範囲を特定し、QA観点をまとめる 13年の歴史を持ち、300個ものViewControllerを抱えるWEARにとって、影響範囲の調査は人力だけでやり切れる規模ではありません。そこで、Claude CodeやGitHub CopilotなどのAIをフル活用し、3つのステップを踏んで対応を進めました。 AIを使って影響範囲を把握する AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す AIにQA観点を書き出させる 1. AIを使って影響範囲を把握する 先ほど触れた どの処理をSceneDelegateへ移すか の表をAIに読み込ませ、影響範囲を特定しました。また、具体的にどんな処理が入っているのかも表にさせたことで、必要な対応の解像度を上げました。 2. AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す どの処理をSceneDelegateへ移すか で洗い出した対象メソッドは、もれなくSceneDelegateへ移す必要があります。しかし、一度にやってしまうと対応漏れが出かねません。AIの出力の質やスピードを上げる意味でも、範囲を絞って1個ずつ移すのが良いと判断しました。 3. AIにQA観点を書き出させる ここが一番助かった部分です。今まで、QAに提出する観点は人の手で書いていました。これを機に、その大半をAIが書く形へ変えました。 具体的な手順は次のとおりです。 まず、AIにSceneDelegate対応をさせてプロトタイプを作り、それをベースに全体の影響範囲とQA観点をざっと把握する メソッド移行ごとにブランチを切り、PR作成時、Diffの内容からJiraへQA観点を出力する プロトタイプで書き出したQA観点とブランチごとのQA観点を見比べて不足がないか確認する こうすることで、人間が気づかなかった範囲も調べて出力してくれるので、質の良いQAにつながりました。 具体的には、特定の地域からアクセスしたときだけ表示する利用規約Windowや、メンテナンス画面の表示・非表示のように複数のライフサイクルイベントにまたがって初めて成立する処理などが対象でした。普段意識する機会が少ない処理ほど、人力でQA観点を洗い出す際には抜け落ちがちです。AIに列挙させたことで、こうした処理も取りこぼさずQA観点に反映できました。 これから対応するならXcode 27の併用も検討を WEARのSceneDelegate対応は2025年に行ったため、Xcode 27を使うことはできませんでした。これから対応する場合は、Xcode 27の併用も検討してみてください。 WWDC26のセッション「 Modernize your UIKit app 」では、Xcode 27同梱の「uikit-app-modernization」スキルが紹介されています。このスキルはSceneDelegate対応だけでなく、UIScreen.mainやinterfaceOrientationのモダナイズなどにも対応しています。今進めているAdaptiveレイアウト対応でも役立っています。 また、Xcode 27は、Appleの公式ドキュメントやテックノートはもちろんのこと、Apple Developer Forumsの情報も参照してくれるため、非常に便利です。正式リリースが待ち遠しいですね。 アプローチ3. リスクを抑えるため、最小限の変更にとどめる 影響範囲の広さに対するもう1つの答えが、移行のスコープを必要最低限にとどめることです。WEARでは以下の方針で移行を進めました。 シーンのライフサイクルにのみ対応する 処理の移行を最優先し、処理のリファクタリングは後で行う マルチウィンドウの対応は一旦見送る SceneDelegate に対応すれば、iPadでのマルチウィンドウなど、できることは増えます。しかし、リスクを最小限に抑えるため、まずシーンのライフサイクル対応だけを完結させると決めました。 また、WEARの AppDelegate はObjective-Cで書かれた非常に古いコードだったため、本音を言えばリファクタリングもしたいところでした。今回、 SceneDelegate へ移行した処理に限りSwiftへの置き換えを行いましたが、処理そのものには手を加えず、挙動を変えない必要最小限の修正にとどめています。 つまずいた箇所と対処 最後に、移行中に直面した代表的な不具合と、その対処を紹介します。 一番最初のウィンドウを取得するのは危ない SceneDelegate 対応後は、 UIWindow が各シーン( UIWindowScene )に紐づき、シーンごとの持ち物へと変わります。 本来は良くない実装ですが、WEARはこれまでSceneDelegateに対応していなかったため、以下のコードでWindowを取得しても問題になっていませんでした。 extension UIApplication { var firstKeyWindow : UIWindow? { let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene return windowScene?.windows.first( where : \.isKeyWindow) } } しかし、SceneDelegate移行後はマルチウィンドウが可能になるため、最初に取得したシーンのWindowが、いま操作しているWindowとは限らなくなります。 よって、ViewControllerからWindowを取得する場合は、 view.window を使って「いま表示しているシーンのWindow」を取得する形へ修正しました。ただし view.window は、Viewがウィンドウ階層に追加されるまでは nil のままなので、呼び出すタイミングには注意が必要です。 // Before: アプリ全体のkeyWindowに依存していた let rootVC = UIApplication.shared.firstKeyWindow?.rootViewController as? RootViewController // After: 自分が表示されているシーンのWindowから取得する let rootVC = view.window?.rootViewController as? RootViewController 外部ソースからの起動判定はconnectionOptionsで行う これまでは AppDelegate の launchOptions のキーで「外部から開かれた起動か」を判定し、ダイアログ表示などを制御していました。 SceneDelegate では、この起動情報は scene(_:willConnectTo:options:) に渡される connectionOptions へ集約されています。型は UIScene.ConnectionOptions です。 これを参照することで、URLスキームやユニバーサルリンク、プッシュ通知など、「どこで起動されたのか」を判別できます。 WEARでは次のように判定しています。 userActivities や urlContexts からディープリンクを取り出して種別を判定します。それらがなくリモート通知での起動であれば .remoteNotification を、いずれにも当てはまらない通常起動なら nil を返します。 extension SceneDelegate { enum ExternalSource { case urlScheme case universalLink case remoteNotification } } extension SceneDelegate.ExternalSource { @MainActor init? (_ connectionOptions : UIScene.ConnectionOptions ) { let userActivityURL = connectionOptions.userActivities.first { $0 .activityType == NSUserActivityTypeBrowsingWeb }?.webpageURL let urlSchemeURL = connectionOptions.urlContexts.first?.url if let deepLinkURL = userActivityURL ?? urlSchemeURL { switch deepLinkURL.scheme { case "URLスキーム名" : self = .urlScheme case "https" : self = .universalLink default : return nil } return } if connectionOptions.notificationResponse != nil { self = .remoteNotification return } return nil } } func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { let externalSource = SceneDelegate.ExternalSource(connectionOptions) } さいごに 「技術調査で役割を正確に理解する」「AIで影響範囲を特定する」「最小限の変更にとどめる」という3つのアプローチにより、 SceneDelegate 対応を無事完了できました。 なかなか骨が折れましたが、 SceneDelegate に対応したことでマルチウィンドウに対応する土台ができ、AIエージェントを使って大規模な修正に取り組む経験も得られました。 この記事が、 SceneDelegate 対応を控えている方の一助になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com

動画

書籍