Objective-C
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
こんにちは、株式会社LuupでEngineering Managerをしている瀧川です。 2026年9月1日(火)〜3日(木)にベルサール渋谷ガーデンで開催されたDroidKaigi 2026に、Androidエンジニアのメンバーといっしょに参加してきました! LuupではLUUPアプリをiOS・Androidともにネイティブで開発しているので、Android開発の最新動向やAI活用の事例をキャッチアップしたく参加してきました。 平日開催のため私は最終日しか参加できず残念でしたが、資料やセッション動画は後日公開されるはずなので、聴けなかったセッションはそちらで追いかけようと思っています。
はじめに こんにちは。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
Web未経験MLエンジニアが社内プロダクト開発でAIコーディングにどハマりするまで はじめに こんに ...
動画
該当するコンテンツが見つかりませんでした








