はじめに
こんにちは、後藤です。株式会社レコチョクでiOSアプリ開発をしています。最近は、ぽこ あ ポケモンとSlay the Spire 2を遊び尽くしています。
さて、iOS 13以降、Appleはアプリのライフサイクル管理をAppDelegateからSceneDelegateへ移行することを推奨してきました。さらに、iOS 26の次のメジャーリリースからは最新SDKでのビルド時にSceneDelegate対応が必須となることが発表されています。
In the next major release following iOS 26, UIScene lifecycle will be required when building with the latest SDK; otherwise, your app won’t launch.
iOS 26の次のメジャーリリースでは、最新のSDKでビルドする際にUISceneライフサイクルが必須となり、対応していないアプリは起動できなくなります。
Migrating to the UIKit scene-based life cycle – Apple Developer
この移行に伴い、URLスキームによる画面遷移の処理コードも書き換えが必要になりました。
URLスキームは外部からアプリの特定画面へ遷移させるために広く使われている機能です。移行時にデグレが発生すると、ユーザーが期待した画面に遷移できなくなります。アプリによってはURLスキームの数が多く、すべてを手動で確認するのは大きな手間になります。
そこで、XCUITestを使ってURLスキームの画面遷移を自動検証することで、SceneDelegate移行時のデグレを防ぐことができると考えました。
本記事では、次の内容を解説します。
- SceneDelegate移行時にURLスキームの起動処理がどう変わるか
- URLスキームのテストで考慮すべき起動種別
- XCUITestでURLスキームの動作を自動検証する方法

対象読者
- SceneDelegate移行を控えているiOSエンジニア
- URLスキームの画面遷移テストを自動化したいエンジニア
実行環境
| 項目 | バージョン |
|---|---|
| Xcode | 26.0.1 |
| iOS | 26.0.1 |
SceneDelegate移行による変更点
AppDelegateでは、URLスキームの処理はapplication(_:open:options:)の1つのメソッドで行っていました。
SceneDelegateへ移行すると、このメソッドの処理をアプリの起動種別に応じた2つのメソッドに分割します。
If your app has opted into Scenes, and your app isn’t running, the system delivers the URL to the
scene(_:willConnectTo:options:)delegate method after launch, and toscene(_:openURLContexts:)when your app opens a URL while running or suspended in memory.アプリがSceneに対応している場合、アプリが起動していなければURLは起動後に
scene(_:willConnectTo:options:)に渡されます。アプリが起動中またはメモリ上でサスペンド中の場合はscene(_:openURLContexts:)に渡されます。
次のセクションでは、アプリの起動種別について解説します。
アプリの起動種別
AppleのWWDCの内容によると、アプリの起動は3種類に分類されます。
| Cold Start | Warm Start | Resume | |
|---|---|---|---|
| プロセス | なし | なし | あり |
| メモリ | なし | 一部キャッシュあり | すべてあり |
| 発生条件 | 端末の再起動後や、アプリが長時間起動されていない場合 | 最近終了した後の再起動 | サスペンド状態からの復帰(ホーム画面やApp Switcherからの再表示など) |
Cold StartとWarm Startはどちらもアプリが起動していない状態です。そのため、どちらもURLスキーム経由で起動した時には、scene(_:willConnectTo:options:)が呼ばれます。
| Launch(Cold / Warm) | Resume | |
|---|---|---|
| Sceneセッション | なし | あり |
| SceneDelegateメソッド | scene(_:willConnectTo:options:) |
scene(_:openURLContexts:) |
つまり、URLスキームの動作を確認するテストで検証すべきは LaunchとResumeの2パターンです。本記事では、Cold StartとWarm Startを合わせてLaunchと記述します。
この移行が正しく行われているかを、XCUITestで検証します。
XCUITestでの実装
テストの構造
URLスキームのテストで検証したいことは「指定したURLで期待する画面に遷移するか」です。遷移先の画面にはaccessibility identifierを付与しておき、XCUITestからその要素の存在を確認することで画面遷移を検証します。
私が携わっているプロジェクトではViewControllerの基底クラスが存在しているので、そこでクラス名をaccessibility identifierとして付与しています。
override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = String(describing: type(of: self))
}
テストごとに異なるのはURL・クエリ・遷移先の画面要素だけなので、共通のプロトコルとして定義します。
import XCTest
/// URLスキームテストに必要な要素を定義するプロトコル
protocol URLSchemeTestable: XCTestCase {
/// スキームのホスト名(例: "albumdetail")
var schemeHost: String { get }
/// クエリパラメータ
var queryItems: [URLQueryItem] { get }
/// 遷移先の画面要素
var targetElement: XCUIElement { get }
/// クエリパラメータの反映を検証する要素
var queryAssertionElement: XCUIElement? { get }
}
targetElementは画面遷移が行われたかを検証するためのプロパティです。accessibility identifierなどを指定することで、期待する画面が表示されたかを確認します。
queryAssertionElementは、クエリパラメータの値が画面に正しく反映されているかを検証するためのプロパティです。たとえば、id=123というクエリパラメータを渡した場合に、そのIDに対応する要素が画面に表示されているかを確認します。デフォルトはnilで、画面遷移の確認だけで十分な場合はqueryItemsも含め不要です。
次に、共通のアサーションメソッドをプロトコルのデフォルト実装として提供します。
extension URLSchemeTestable {
var app: XCUIApplication { XCUIApplication() }
var queryAssertionElement: XCUIElement? { nil }
var schemeURL: URL {
var components = URLComponents()
components.scheme = "myapp"
components.host = schemeHost
components.queryItems = queryItems
return components.url!
}
/// Launchのテスト
func assertLaunch() {
app.terminate()
app.open(schemeURL)
XCTAssertTrue(targetElement.waitForExistence(timeout: 10))
if let element = queryAssertionElement {
XCTAssertTrue(element.exists)
}
}
/// Resumeのテスト
func assertResume() {
app.launch()
// Safari経由でURLスキームを開く
openURLViaSafari(schemeURL)
XCTAssertTrue(targetElement.waitForExistence(timeout: 10))
if let element = queryAssertionElement {
XCTAssertTrue(element.exists)
}
}
/// Safari経由でURLスキームを開く
private func openURLViaSafari(_ url: URL) {
let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
safari.launch()
safari.open(url)
let openButton = safari.buttons["開く"]
if openButton.waitForExistence(timeout: 3) {
openButton.tap()
}
}
}
(補足)ResumeのテストでSafariを経由する理由
コードの中でSafariを経由してURLスキームを開いていますが、これには理由があります。当初はapp.open(url)を連続して呼ぶことでResumeをテストしようとしましたが、うまくいきませんでした。
// この書き方ではResumeをテストできない
app.open(url) // 1回目: アプリ起動 → scene(_:willConnectTo:options:) が呼ばれる
app.open(url) // 2回目: アプリが終了 → 再度 scene(_:willConnectTo:options:) が呼ばれる
XCUITestでapp.open(url)を呼び出すと、呼び出し前にアプリが一度終了されるような挙動になります。2回目のapp.open(url)はscene(_:openURLContexts:)ではなくscene(_:willConnectTo:options:)を呼び出します。つまり、Resumeではなく再びLaunchとして処理されます。
そのため、ResumeのテストではSafari経由でURLスキームを開く方法を採用しています。
テストケースの実装
各URLスキームのテストは、プロトコルに準拠して次のように実装しています。 下記はクエリパラメータも検証している例です。
import XCTest
final class AlbumDetailSchemeTests: XCTestCase {
func test_Launch_albumdetailスキームでアルバム詳細画面が表示される() {
assertLaunch()
}
func test_Resume_albumdetailスキームでアルバム詳細画面に遷移する() {
assertResume()
}
}
extension AlbumDetailSchemeTests: URLSchemeTestable {
var schemeHost: String { "albumdetail" }
var queryItems: [URLQueryItem] { [URLQueryItem(name: "id", value: "123")] }
var targetElement: XCUIElement { app.otherElements["AlbumDetailViewController"] }
var queryAssertionElement: XCUIElement? { app.staticTexts["Album ID: 123"] }
}
これらのテストをSceneDelegate移行前に実装しておくことで、移行作業を安心してスムーズに進めることができます。SceneDelegate移行の予定がない場合でも、URLスキームのデグレ検知に活用できます。
なお、XCUITestではUI要素の種類ごとに異なるプロパティを使い分けます。今回のコードの例ではUIViewを特定するotherElementsとUILabelを特定するstaticTextsを使用しています。
その他のプロパティはXCUIElementTypeQueryProviderをご覧ください。
動作イメージ
| Launch | Resume |
|---|---|
![]() |
![]() |
まとめ
SceneDelegate移行では、URLスキームの処理経路がAppDelegateから変わります。Appleの定義ではアプリの起動はCold Start・Warm Start・Resumeの3種類です。そして、URLスキームのテスト観点で重要な分岐はLaunchとResumeの2つです。
XCUITestを用意しておくことで、移行前後の動作を自動で検証できます。URLスキームが多数定義されているアプリでは、手動確認のコストが大きいため、テストの自動化による恩恵は大きいです。
本記事が皆様のSceneDelegate移行対応の手助けとなれば幸いです。
参考
後藤新
