iOS の面接対策

iOS 面接問題

よく出る iOS の面接問題20問。iOS 開発は、Swift、SwiftUI、UIKit アプリを構築し、ライフサイクル、並行処理、ネットワーク、ストレージ、性能、テスト、App Store リリースの課題を解決する、需要の高い分野です。問題は難易度別に用意されており、面接トレーナーで声に出して回答の練習ができます。

iOS の AI 面接を開始クレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

初級者向け質問

1典型的なiOSのMVC (Model-View-Controller) アーキテクチャにおいて、UIViewControllerはどのような責務を持つべきですか?

従来のiOSのMVC(Model-View-Controller)において、UIViewControllerはViewコンポーネントとModelデータの橋渡しをするControllerとして機能します。主な責務には、Viewのライフサイクル管理(`viewDidLoad` や `viewWillAppear` など)、UI要素の初期設定と更新、直接的なユーザー操作のハンドリング(ボタンタップ、デリゲート、ターゲット・アクションなど)、および画面遷移やプレゼンテーションの制御が含まれます。 直接的なネットワーク通信、データの永続化、重いビジネスロジックなどのUI以外の責務は、専用のサービスオブジェクトやモデルオブジェクトに委譲すべきです。これにより、View Controllerが肥大化してMassive View Controllerになるのを防ぐことができます。

class UserProfileViewController: UIViewController {
    private let userService: UserServiceProtocol
    private let profileView = UserProfileView()
    
    init(userService: UserServiceProtocol) {
        self.userService = userService
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    
    override func loadView() {
        view = profileView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        profileView.editButton.addTarget(self, action: #selector(didTapEdit), for: .touchUpInside)
        fetchProfile()
    }
    
    private func fetchProfile() {
        userService.fetchCurrentUser { [weak self] result in
            DispatchQueue.main.async {
                if case .success(let user) = result {
                    self?.profileView.configure(with: user)
                }
            }
        }
    }
    
    @objc private func didTapEdit() {
        // Handle user action or trigger navigation
    }
}
AI コーチを使ってこの質問に答えてみる

2iOSアプリのリリースにおいて、プロビジョニングプロファイルはどのような役割を果たしますか?

iOSのリリースにおいて、プロビジョニングプロファイルはAppleのセキュリティ要件とアプリのバイナリおよび対象デバイスを橋渡しする、暗号署名されたパッケージとして機能します。その主な役割は、アプリケーションが特定の配信ルールに基づいて実行を許可されていることを、iOSオペレーティングシステムおよびApp Store Connectに伝えることです。 プロビジョニングプロファイルは、以下の重要な要素をひとまとめにバンドルしています。 - App ID(Bundle Identifier) - アプリのビルド元を確認する承認済みの署名証明書 - アプリに付与されたエンタイトルメントと機能(プッシュ通知、Sign in with Apple、iCloudなど) - App Store以外のプロファイル(DevelopmentやAd-Hocなど)の場合、許可されたデバイスのUDIDリスト App Storeリリース用プロファイルでは、App Storeを通じて任意の一般ユーザーのデバイスへ配信することがAppleによって許可されているため、個々のデバイスUDIDは省略されます。

Provisioning Profile (.mobileprovision)
├── App ID / Bundle Identifier (e.g., com.example.app)
├── Certificates (Public key / Distribution Certificate)
├── Entitlements (e.g., Push Notifications, Associated Domains)
└── Device UDIDs (Present for Development/Ad-Hoc; omitted for App Store distribution)
AI コーチを使ってこの質問に答えてみる

3iOSアプリがバックグラウンドで処理を実行するとはどういう意味ですか?また、システムはその処理に対してどのような基本的な制限を設けていますか?

iOSにおけるバックグラウンドでの処理実行とは、アプリが画面上でアクティブになっていない、またはユーザーから見えていない状態のときにコードを実行することを指します。ユーザーがアプリを離れると、アプリはアクティブなフォアグラウンド状態からバックグラウンド状態へと遷移し、その後間もなくサスペンド状態へと移行します。サスペンド状態ではメモリ(RAM)上に展開されたまま実行のみが一時停止されます。 バッテリー駆動時間、システムの応答性、およびデバイスリソースを保護するため、iOSはバックグラウンド実行を厳格に制御しています。システムはバックグラウンド移行後にコードを実行できる時間(特定のバックグラウンドモードやスケジュールタスクを使用しない限り、通常は約30秒程度の短い有限なウィンドウ)を制限し、CPUやネットワークの優先度を抑制(スロットリング)するほか、デバイスのメモリ逼迫時にはサスペンド状態のアプリを終了させることがあります。

import UIKit

NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification,
    object: nil,
    queue: .main
) { _ in
    print("App entered background. Code execution will pause soon unless extended.")
}
AI コーチを使ってこの質問に答えてみる

4Combineにおけるパブリッシャー(Publisher)とは何ですか。また、一般的なiOSのデータフローにおいてサブスクライバー(Subscriber)とはどのように異なりますか?

Combineにおいて、Publisherは時間の経過に伴って一連の値(ストリーム)を発行し、完了イベント(正常終了またはエラーによる失敗)で終了できます。Publisherは2つの関連型を宣言します。生成するデータの型である`Output`と、発行する可能性があるエラーの型である`Failure`です。Subscriberは、パブリッシャーから値とライフサイクルイベントを受け取ります。典型的なiOSのデータフローでは、パブリッシャーは非同期イベント(ネットワークレスポンス、通知、ユーザー入力など)の発行元(プロデューサー)として機能し、サブスクライバーは発行された値への対応、エラー処理、完了への対応を行うコンシューマーとして機能します(UI状態の更新やデータベースへの書き込みなど)。サブスクライバーはパブリッシャーにアタッチ(接続)し、サブスクリプショントークンを受け取り、要素の要求(demand)を行います。

import Combine

// Publisher: emits a sequence of integers
let numberPublisher = [1, 2, 3].publisher

// Subscriber: consumes the emitted integers
let cancellable = numberPublisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: \(error)")
        }
    },
    receiveValue: { value in
        print("Received: \(value)")
    }
)
AI コーチを使ってこの質問に答えてみる

5Swiftにおける `async/await` にはどのような意味があり、iOSアプリから非同期APIを呼び出すにはどのように使用しますか?

`async/await` は、ネストされたクロージャや完了ハンドラ(completion handler)を使用する代わりに、非同期コードを直線的かつ可読性の高いシーケンシャルな形式で記述するためのSwift組み込み構文です。関数に `async` を指定すると、時間のかかる処理(ネットワークリクエストやファイルI/Oなど)の完了を待つ間、その関数の実行を中断(サスペンド)できることをコンパイラに伝えます。`await` キーワードは中断ポイントを示します。現在の関数の実行が一時停止し、基礎となるスレッドが解放されて他の処理を実行できるようになり、待機していた結果やエラーの準備が整うと実行が再開されます。iOSアプリから非同期APIを呼び出すには、別の `async` 関数の内部や `Task` ブロックなどの非同期コンテキスト内から、呼び出す `async` メソッドの前に `await`(エラーをスローする関数の場合は `try await`)を付与して呼び出します。

func fetchUserData(from url: URL) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    
    return try JSONDecoder().decode(User.self, from: data)
}
AI コーチを使ってこの質問に答えてみる

6iOSアプリでUserDefaultsはどのような場合に使用すべきですか?また、どのようなデータをそこに保存すべきではありませんか?

UserDefaultsは、アプリの起動を跨いで保持する小規模で軽量なユーザー設定やフラグ(テーマ設定、音量レベル、`hasSeenOnboarding` のようなブール値フラグなど)を保存するために設計されています。String、Int、Double、Bool、Date、Data、Array、Dictionary などのプロパティリスト型をサポートしています。 保存すべきではないデータは以下の通りです: 1. 機密情報(ユーザーパスワード、秘密鍵、認証トークンなど): UserDefaultsはデータを暗号化せず、プレーンテキストのplistファイルに保存するためです。 2. 大規模なデータセット、ドキュメント、またはメディアファイル(画像、音声、サイズの大きいJSONペイロードなど): アクセス時にplist全体がメモリにロードされるため、メモリのオーバーヘッドが大きくなり、アプリの起動時間が遅延する原因になります。

// Saving simple preference values
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
UserDefaults.standard.set("dark", forKey: "preferredTheme")

// Reading values back
let hasCompletedOnboarding = UserDefaults.standard.bool(forKey: "hasCompletedOnboarding")
let theme = UserDefaults.standard.string(forKey: "preferredTheme") ?? "system"
AI コーチを使ってこの質問に答えてみる

7SwiftにおけるARC(Automatic Reference Counting)とは何ですか?また、iOSアプリ内でクラスインスタンスの寿命をどのように管理しますか?

ARC(Automatic Reference Counting)は、クラスインスタンス(参照型)の寿命を追跡・管理するためのSwiftのコンパイル時メモリ管理機構です。クラスの新しいインスタンスが生成されて強参照(strong reference)に代入されるたびに、ARCはそのインスタンスの内部参照カウントを追跡します。インスタンスへの強参照が少なくとも1つ存在する限り、ARCはそのインスタンスをメモリ上に保持します。すべての強参照が解除されて参照カウントがゼロになると、ARCは即座にインスタンスを破棄してメモリを解放し、解放の直前にインスタンスの`deinit`メソッドを自動的に呼び出します。Javaや.NETのような環境における実行時ガベージコレクション(GC)とは異なり、ARCは定期的なスイープサイクルを実行しません。Swiftコンパイラがコンパイル時にバイナリへ適切なretain呼び出しとrelease呼び出しを挿入します。

class User {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    deinit {
        print("\(name) deallocated")
    }
}

var ref1: User? = User(name: "Alice") // count = 1
var ref2: User? = ref1               // count = 2

ref1 = nil                           // count = 1
ref2 = nil                           // count = 0 -> deinit called
AI コーチを使ってこの質問に答えてみる

8iOSアプリにおいて、URLSession を使用してシンプルな HTTP (Hypertext Transfer Protocol) GET リクエストを実行するにはどうすればよいですか?

iOSアプリでシンプルな HTTP GET リクエストを実行するには、URL オブジェクトを作成し、URLSession.shared などの URLSession インスタンスに渡します。リクエストの実行には、データタスクの完了ハンドラ(URLSession.shared.dataTask(with: url) { data, response, error in ... })を使用する方法と、モダンな Swift の並行処理(let (data, response) = try await URLSession.shared.data(from: url))を使用する方法があります。従来のデータタスクによる手法を使用する場合は、タスクを開始するために .resume() を呼び出し、トランスポートエラーを処理し、HTTPURLResponse のステータスコードを確認した上で、UIの更新処理が必ずメインスレッドにディスパッチされるようにする必要があります。

guard let url = URL(string: "https://api.example.com/items") else { return }

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
        print("Network error: \(error.localizedDescription)")
        return
    }
    
    guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
        print("Server error or invalid status code")
        return
    }
    
    if let data = data {
        DispatchQueue.main.async {
            // Update UI on main thread
        }
    }
}
task.resume()
AI コーチを使ってこの質問に答えてみる

9XcodeのInstrumentsとは何ですか?また、iOSアプリにおける単純なパフォーマンス問題を調査するためにどのように使用しますか?

Instrumentsは、Xcodeに同梱されているAppleのパフォーマンスプロファイリングおよび分析ツールです。ランタイムの動作、CPU使用率、メモリ割り当て、メモリリーク、UIレンダリングのパフォーマンスなどを監視できます。UIのカクつきや高いCPU使用率といった単純なパフォーマンス問題を調査する場合、XcodeからInstrumentsを起動し(Product -> Profile)、適切なテンプレート(CPUボトルネックにはTime Profiler、メモリ問題にはAllocationsやLeaksなど)を選択して、実機上で問題となるユーザーフローを再現しながらトレースを記録します。記録完了後、コールツリー(call tree)や最も負荷の高いスタックトレースを分析することで、遅延や過度なリソース消費を引き起こしている具体的なメソッドを特定できます。これにより、コード変更を行う前にボトルネックを正確に測定・特定できます。

1. In Xcode, select Product -> Profile (Builds with Release optimizations).
2. Select the 'Time Profiler' template.
3. Click Record and perform the sluggish action in the app.
4. Stop recording.
5. Inspect the Call Tree tab:
   - Enable 'Separate by Thread' and 'Hide System Libraries'.
   - Trace the heaviest call stack on the Main Thread to identify the blocking function.
AI コーチを使ってこの質問に答えてみる

10APNs (Apple Push Notification service) とは何ですか。また、iOSアプリにプッシュ通知を配信するうえでどのような役割を果たしますか?

APNs(Apple Push Notification service)は、バックエンドサーバー(プロバイダサーバー)からApple製デバイスへプッシュ通知を安全にルーティングする、Appleのクラウドベースの仲介サービスです。モバイルデバイスが各アプリのバックエンドと個別に常時ソケット接続を維持すると、バッテリーの急激な消耗や過度なネットワーク帯域消費が発生するため、Appleは各デバイスとAPNsの間に低消費電力の持続的接続を1本だけ維持しています。アプリがリモート通知に登録すると、APNsはそのデバイス上の特定のアプリインストールを一意に識別する不透明なデバイストークンを生成します。アプリはこのトークンをバックエンドのプロバイダサーバーに送信します。プロバイダがユーザーに通知を送りたい場合、ペイロードを構築し、HTTP/2経由でデバイストークンとともにAPNsへ送信します。APNsはそのデバイスのアクティブな接続を特定し、プッシュ通知のペイロードをiOSへ直接配信します。その後、iOSが設定に応じてアラートを表示したりアプリを起床させたりします。

[Provider Server] ---> (HTTP/2 Request + Device Token + Payload) ---> [APNs]
                                                                           |
                                                               (Persistent APNs Connection)
                                                                           v
                                                                    [iOS Device / App]
AI コーチを使ってこの質問に答えてみる

中級者向け質問

11UI(User Interface)の更新、バリデーション、ネットワーク通信、および画面遷移を抱え込んだ肥大化したUIViewControllerをどのようにリファクタリングしますか?

いわゆる肥大化したView Controller(Massive View Controller / MVC)のリファクタリングは、リスクを最小限に抑えるため段階的に進め、個別の責務を専用のレイヤーへと分離します。 1. ネットワークとデータアクセスの分離: API呼び出し、データの永続化、JSONパースの処理をViewControllerから切り離し、プロトコルで抽象化した専用のServiceクラスやRepositoryクラスへ移行します。 2. プレゼンテーション状態とバリデーションの分離: ViewModel(またはPresenter)を導入します。入力値の検証、文字列のフォーマット、UI状態の管理をViewModelに移譲し、これらのロジックを単体テスト可能な状態にします。 3. 画面遷移ロジックの分離: Coordinatorパターン(またはRouter / Flow Controller)を採用し、`push` や `present` の呼び出しおよび画面遷移フローの制御ロジックをViewControllerから取り除きます。 4. ViewControllerの軽量化(Lean化): ViewControllerの責務を、UIレイアウト、サブビューの構成、ライフサイクルフック、およびUIコンポーネントとViewModelとのバインディングのみに絞り込みます。 5. テストを伴う段階的な移行: リファクタリングの過程で新しく抽出したServiceやViewModelに対する単体テストを追加し、既存の動作が変わっていないことを担保しながら進めます。

// 1. Extracted Navigation
protocol LoginCoordinatorProtocol: AnyObject {
    func showHomeScreen()
}

// 2. Extracted Business Logic & State
final class LoginViewModel {
    private let authService: AuthServiceProtocol
    private weak var coordinator: LoginCoordinatorProtocol?
    
    init(authService: AuthServiceProtocol, coordinator: LoginCoordinatorProtocol) {
        self.authService = authService
        self.coordinator = coordinator
    }
    
    func login(email: String, password: String) {
        guard email.contains("@") else { return }
        authService.login(email: email, password: password) { [weak self] result in
            if case .success = result { self?.coordinator?.showHomeScreen() }
        }
    }
}

// 3. Lean UIViewController: Only handles UI and bindings
final class LoginViewController: UIViewController {
    private let viewModel: LoginViewModel
    init(viewModel: LoginViewModel) { self.viewModel = viewModel; super.init(nibName: nil, bundle: nil) }
    required init?(coder: NSCoder) { fatalError() }
}
AI コーチを使ってこの質問に答えてみる

12署名やプロビジョニングのエラーによってiOSアーカイブのアップロードに失敗した場合、どのようにトラブルシューティングを行いますか?

署名やプロビジョニングの問題によるiOSアーカイブのアップロード失敗をトラブルシューティングする際は、主に配布用証明書、プロビジョニングプロファイル、ターゲットのEntitlements(エンタイトルメント)の3つの領域を確認します。 1. 診断ログの確認: Xcode Organizerの配布ログ、アップロード検証時の詳細なエラー、またはCI(Continuous Integration)のコンソールログ(`altool`/`notarytool`)を確認し、拒否された正確な原因を特定します。 2. 証明書とIDの検証: キーチェーンに対応する秘密鍵を含む有効なApple Distribution証明書が存在し、期限切れや失効になっていないこと、Apple WWDR(Worldwide Developer Relations)中間証明書が不足していないことを確認します。 3. プロビジョニングプロファイルの整合性: エクスポートに使用されたプロファイルが、正確なBundle Identifierに一致するApp Store配布用プロファイルであることを確認します。プッシュ通知やAssociated Domainsなどの機能を使用している場合は、互換性のないワイルドカードではなく明示的なApp IDが設定されているかを検証します。 4. Entitlementsの整合性: ターゲットの `.entitlements` ファイルと、Apple Developer Portal上のApp IDで有効化されている機能との間に不整合がないか確認します。ローカルのEntitlementsで要求している権限がポータル側のプロファイルに登録されていない場合、アップロードは失敗します。 5. 署名設定: 自動署名を使用している場合は、Xcodeの「Settings」で選択されているチームとApple IDの認証情報を確認します。手動署名(またはCIのエクスポートオプション)を使用している場合は、`ExportOptions.plist` 内で各Bundle IDが正しい配布用証明書とプロファイルに対応付けられていることを確認します。

# Decode the provisioning profile embedded in the archive
security cms -D -i MyApp.xcarchive/Products/Applications/MyApp.app/embedded.mobileprovision > profile.plist

# Read entitlements embedded in the profile
/usr/libexec/PlistBuddy -c "Print :Entitlements" profile.plist

# Inspect signature and entitlements on the built binary
codesign -d --entitlements :- MyApp.xcarchive/Products/Applications/MyApp.app
AI コーチを使ってこの質問に答えてみる

13本番環境のバックグラウンド同期機能において、BGAppRefreshTask、BGProcessingTask、およびバックグラウンドURLSessionをどのように使い分けますか?

適切なバックグラウンドAPIの選択は、タスクの実行時間、電源やネットワーク条件が必要かどうか、およびネットワークペイロードの性質によって決まります。 1. **BGAppRefreshTask**: ニュースフィード、SNSのタイムライン、ユーザーダッシュボードの更新など、約15〜30秒程度で完了する短時間で軽量なコンテンツ更新に最適です。システムはユーザーの使用パターンに基づいてスケジュールを設定し、ユーザーがアプリを開く直前に最新のコンテンツが準備されるようにします。 2. **BGProcessingTask**: データのインデックス作成、データベースのクリーンアップやマイグレーション、機械学習モデルの学習、大規模なデータ同期など、実行時間が長く緊急性の低い重い処理向けに設計されています。数分間実行でき、デバイスの充電中(`requiresExternalPower = true`)やWi-Fi/ネットワーク接続中(`requiresNetworkConnectivity = true`)であることを実行条件に指定でき、通常は夜間に実行されます。 3. **バックグラウンドURLSession (`URLSessionConfiguration.background`)**: 写真や動画のアップロード、大容量アセットバンドルのダウンロードなど、アプリがOSによってサスペンドまたは終了された場合でもアウトオブプロセス(別プロセス)で継続する必要がある大容量ファイル転送における正しい選択肢です。アプリのコードをメモリ上でアクティブに保ち続けるのではなく、ネットワーク転送を `nsurlsessiond` デーモンにオフロードします。

import BackgroundTasks

func scheduleDatabaseMaintenance() {
    let request = BGProcessingTaskRequest(identifier: "com.app.db_cleanup")
    request.requiresNetworkConnectivity = false
    request.requiresExternalPower = true // Run overnight on charger
    request.earliestBeginDate = Date(timeIntervalSinceNow: 6 * 3600)
    
    try? BGTaskScheduler.shared.submit(request)
}

func scheduleTimelineRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.feed_refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    
    try? BGTaskScheduler.shared.submit(request)
}
AI コーチを使ってこの質問に答えてみる

14入力のデバウンス、重複の無視、ネットワークリクエストの実行、およびUI(User Interface)の安全な更新を行う検索テキストフィールド用のCombineパイプラインをどのように構築しますか?

Combineで安全な検索パイプラインを構築する手順は次のとおりです。 1. **デバウンスと重複排除:** 検索クエリのパブリッシャー(`$queryText` など)に対して、タイピングの休止を待つために `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` を適用し、変更のないクエリ文字列を無視するために `.removeDuplicates()` を適用します。 2. **ネットワークリクエストとキャンセル処理:** 各クエリをネットワークリクエストのパブリッシャーにマッピングし、`.switchToLatest()` を用いてフラット化します。これにより、新しい検索クエリが届いた際に実行中のリクエストが自動的にキャンセルされ、競合状態や古い検索結果の反映を防ぐことができます。 3. **エラーの分離:** ネットワークエラーが外側の検索テキストストリーム自体を終了させてしまわないよう、内側のパブリッシャー内でエラーを捕捉(catch)します(例: `.catch { _ in Just([]) }`)。 4. **メインスレッドへのディスパッチ:** 状態の更新やUIプロパティへの代入を行う前に、`.receive(on: DispatchQueue.main)` を適用します。

import Combine
import Foundation

class SearchViewModel: ObservableObject {
    @Published var query: String = ""
    @Published var searchResults: [String] = []
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $query
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .map { query -> AnyPublisher<[String], Never> in
                guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
                    return Just([]).eraseToAnyPublisher()
                }
                return self.performSearch(query)
                    .catch { _ in Just([]) }
                    .eraseToAnyPublisher()
            }
            .switchToLatest()
            .receive(on: DispatchQueue.main)
            .assign(to: &$searchResults)
    }
    
    private func performSearch(_ term: String) -> AnyPublisher<[String], Error> {
        let url = URL(string: "https://api.example.com/search?q=\(term)")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}
AI コーチを使ってこの質問に答えてみる

15Swiftのasync/awaitとTaskのキャンセル処理を使用して、ネットワークからデータを読み込み、ユーザーが画面を離れた後にUI(User Interface)が更新されないようにする画面をどのように実装しますか?

ネットワーク通信処理をサービス層またはViewModel上の`async`関数として実装し、画面のライフサイクルに紐づく`Task`から実行します。UIKitでは、通常`var loadTask: Task<Void, Never>?`のような変数をViewControllerまたはViewModelに保持し、画面の読み込み時に開始して、要件に応じた寿命に合わせて`viewWillDisappear`、`deinit`、または新しいリクエストを開始する直前にキャンセルします。SwiftUIでは、Viewが非表示になった際やidが変更された際に自動的にキャンセルされるため、可能であれば`.task`または`.task(id:)`を優先して使用します。Taskの内部では、`try await`を用いて非同期のネットワークAPIを呼び出し、実際の通信エラーとキャンセルを分けて処理します。また、複数のawaitや処理ステップが存在する場合は、結果を反映する前にキャンセルの状態を確認します。UI状態の更新は、ViewModelに`@MainActor`を付与するか、`await MainActor.run`を使用することで、必ずメインアクター上で行う必要があります。古いUIの描画を防ぐため、以前のタスクのキャンセル、画面に紐づく処理におけるdetached/globalタスクの回避、そして必要に応じて結果反映前にリクエストIDや対象アイテムのIDを照合する処理を行います。

@MainActor
final class UsersViewController: UIViewController {
    private var loadTask: Task<Void, Never>?
    private let service: UserService

    override func viewDidLoad() {
        super.viewDidLoad()
        loadUsers()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        loadTask?.cancel()
    }

    private func loadUsers() {
        loadTask?.cancel()
        loadingView.isHidden = false

        loadTask = Task { [weak self] in
            do {
                let users = try await self?.service.fetchUsers() ?? []
                try Task.checkCancellation()

                guard let self else { return }
                self.loadingView.isHidden = true
                self.tableModel = users
                self.tableView.reloadData()
            } catch is CancellationError {
                // User navigated away or a new load replaced this one; do not update UI.
            } catch {
                guard let self else { return }
                self.loadingView.isHidden = true
                self.showError(error)
            }
        }
    }
}
AI コーチを使ってこの質問に答えてみる

16アプリデータの種類に応じて、UserDefaults、Keychain、ファイル、SQLite、Core Dataをどのように使い分けますか?

適切なiOSストレージメカニズムの選択は、データの機密性、サイズ、リレーションの複雑さ、クエリ要件、およびバックアップのライフサイクルに依存します。 1. **Keychain**: 機密データ(認証トークン、パスワード、暗号化キー、生体認証シークレット)向けです。ハードウェア支援による暗号化を提供し、アプリの再インストール後も保持され、サンドボックス環境が侵害された場合でも安全性を維持します。 2. **UserDefaults**: 軽量で機密性のない設定やフラグ(`hasCompletedOnboarding`、テーマ設定など)向けです。プロパティリストとして全体がメモリに読み込まれるため、大規模なデータセット、メディア、機密トークンには使用すべきではありません。 3. **ファイルシステム(Documents / Caches / Application Support)**: 個別のファイルや大容量バイナリデータ(ダウンロードした画像、音声、PDFなど)向けです。`Documents`はユーザー向けでiCloudにバックアップされます。`Caches`は削除可能で再ダウンロード可能なコンテンツ向けです。`Application Support`はユーザーに直接見せない永続ファイル向けです。 4. **SQLite**: オブジェクトグラフのオーバーヘッドなしで、インデックスを活用した高速なクエリ、一括操作、またはクロスプラットフォーム間でのデータベース共有が必要な構造化された表形式データ向けです。 5. **Core Data / SwiftData**: リレーション、変更追跡、フォールティング(faulting)、取り消し(undo)管理、およびUIKit(`NSFetchedResultsController`)やSwiftUI(`@Query`)との直接連携を伴う複雑なオブジェクトグラフ向けです。

- Auth tokens / API keys          -> Keychain (Hardware-backed encrypted store)
- App settings / UI flags         -> UserDefaults (Lightweight key-value, non-sensitive)
- Downloaded videos / PDFs        -> File System (Documents / Caches directory)
- 50k items with complex queries   -> SQLite / Core Data / SwiftData (Indexed, relationship-aware)
AI コーチを使ってこの質問に答えてみる

17ビューモデルからのコールバックを伴う本番環境のUIKit画面において、クロージャハンドラ内でselfをweak、unowned、またはstrongのいずれとしてキャプチャするかをどのように判断しますか?

ビューコントローラがコールバックを介してビューモデルと連携するUIKitアーキテクチャでは、キャプチャ戦略は所有権とライフサイクルによって決まります。 1. `[weak self]`:保持されるクロージャやエスケープするクロージャ(ビューモデルのイベントコールバックや非同期のネットワーク/データ完了ハンドラなど)に対する標準的かつ最も安全な手法です。ビューコントローラがビューモデルを所有しているため、ビューモデルに保持されるコールバック内で `self` を強参照でキャプチャすると循環参照(`VC -> VM -> closure -> VC`)が発生します。`[weak self]` は `self` をオプショナル型(`UIViewController?`)に変換するため、`self` が正常にメモリ解放され、メモリリークを防ぐことができます。 2. `[unowned self]`:クロージャの実行時に `self` が決してnilにならないことを前提とします。UI画面では細心の注意を払って使用する必要があります。ビューコントローラが破棄・解放された後に非同期処理やコールバックが完了した場合、`unowned self` へのアクセスは致命的な実行時クラッシュを引き起こします。そのため、本番環境のUIKitにおけるUIコールバックでは、一般に `unowned` よりも `[weak self]` が推奨されます。 3. 強参照キャプチャ(デフォルト、キャプチャリストなし):クロージャがエスケープしない場合(`map`/`filter` などの標準的なコレクション操作)や、クロージャが短命で `self` への所有権サイクルを形成しない場合に適しています。

final class ProfileViewController: UIViewController {
    private let viewModel: ProfileViewModel

    init(viewModel: ProfileViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError() }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // VM stores the callback -> capture weak self to break retain cycle
        viewModel.onDataUpdated = { [weak self] state in
            guard let self else { return }
            self.updateUI(with: state)
        }
    }

    private func updateUI(with state: ProfileState) { /* Update views */ }
}
AI コーチを使ってこの質問に答えてみる

上級者向け質問

18数百の画面、多数の Massive View Controller、遅いリリースサイクルを抱える成熟したiOSアプリに参画することになりました。機能提供を停止することなく段階的にアーキテクチャを改善する戦略をどのように計画しますか?

効果的な段階的移行戦略では、全面的な刷新を避け、進行中の機能開発とリファクタリングを直接結びつけます(Strangler Figパターンに従う)。まず、合意された目標アーキテクチャの青写真(依存性の注入とモジュール化されたCoordinatorを備えたMVVMやVIPERなど)を確立し、レガシーコードに手を入れる前にその周辺のテスト基準(仕様確定テスト、スナップショットテスト、ユニットテスト)を整備します。優先順位付けは機能の変更頻度とリスクに基づいて行うべきであり、安定しているレガシーコードではなく、チームが頻繁に変更している画面や不具合率の高い領域からリファクタリングします。Massive View Controllerをリファクタリングする際は、ビジネスロジックと表示ロジックを専用のViewModel/Presenterに分離し、プロトコルベースのアダプターやCoordinatorを導入してレガシーUIを新しいコードから隔離します。最後に、模範的なリファレンス実装の提供、CIリンターやPRガイドラインによる境界の強制、ビルド時間・クラッシュ率・テストカバレッジなどの指標を追跡しながらの継続的な技術的負債へのリソース割り当て(例:15〜20%のキャパシティ確保や関連機能開発との併行リファクタリング)といったアーキテクチャガバナンスを通じて、開発速度と品質を維持します。

protocol ProfileDisplaying: AnyObject {
    func updateProfile(name: String, avatarUrl: URL?)
}

extension LegacyProfileViewController: ProfileDisplaying {
    func updateProfile(name: String, avatarUrl: URL?) {
        self.nameLabel.text = name
    }
}

final class ProfilePresenter {
    private weak var view: ProfileDisplaying?
    private let userUseCase: FetchUserUseCaseProtocol
    
    init(view: ProfileDisplaying, userUseCase: FetchUserUseCaseProtocol) {
        self.view = view
        self.userUseCase = userUseCase
    }
    
    func onViewLoaded() async {
        guard let user = try? await userUseCase.execute() else { return }
        await MainActor.run {
            view?.updateProfile(name: user.name, avatarUrl: user.avatarURL)
        }
    }
}
AI コーチを使ってこの質問に答えてみる

19データベースマイグレーションとバックエンドのAPI変更を伴う高トラフィックなiOSアプリをリリースする場合、ユーザーへの影響を最小限に抑えるロールアウト計画をどのように設計しますか?

データベースマイグレーションとバックエンドのAPI変更を伴う高トラフィックなiOSアプリのロールアウト計画を設計する際は、iOSクライアント特有の制約(ユーザーのアップデートタイミングが非同期であること、およびクライアント側バイナリの強制的なロールバックが不可能なこと)から、デプロイと機能の有効化を切り離す必要があります。 1. バックエンドの下位互換性: Expand-and-Contract戦略(並行稼働による拡張と縮退)を適用します。API v1と並行してAPI v2をデプロイし、旧バージョンと新バージョンの両方のクライアントが破壊的変更なしに同時に動作できるようにします。 2. 堅牢なデータベースマイグレーション: ローカルスキーマのマイグレーション(SQLite、Core Data、SwiftDataなど)は冪等かつ非破壊的とし、メインスレッドをブロックしたり起動時のクラッシュループを引き起こしたりすることなく、複数バージョンをまたぐ移行(例: N-3からNへのアップグレード)を安全に処理できるようにします。 3. 動的な機能制御: 新しいクライアント機能は、サーバー主導のフィーチャーフラグやリモート設定の配下に隠した状態(ダークローンチ)でリリースします。最初の配信時点ではフィーチャーフラグを無効のままにしておきます。 4. 段階的ロールアウトと監視: App Storeの段階的リリース(7日間のフェーズドリリース)を利用してアプリを配信します。テレメトリ、クラッシュ率、データベースマイグレーションの成功率、APIエラー率を継続的に監視します。異常が発生した場合は、緊急のバイナリロールバックを必要とせず、App Store Connectで段階的リリースを一時停止し、フィーチャーフラグをオフに切り替えます。

Phase 1 (Backend Expand): Deploy API v2 supporting both new and legacy payloads.
Phase 2 (Client Distribution & Migration): Release client v2.0 via App Store Phased Release (1% -> 100%). DB migrates safely on first launch. Feature flag remains OFF.
Phase 3 (Validation & Feature Enablement): Monitor crash rates & migration success. Incrementally ramp Remote Config flag (10% -> 50% -> 100%).
Phase 4 (Backend Contract): Once legacy app adoption drops below deprecation threshold, sunset API v1.
AI コーチを使ってこの質問に答えてみる

20複数の端末間で編集内容が最終的に同期されるメモアプリで、iOSがバックグラウンド処理を遅延またはキャンセルする可能性がある状況において、オフラインファーストの同期を設計します。どのようなバックグラウンド実行アーキテクチャを選択しますか?

オフラインファーストの同期アーキテクチャでは、ローカルデータベース(SQLite、Core Data、SwiftDataなど)をUI状態に対する即座の単一の信頼できる情報源(Single Source of Truth)として扱い、未送信の変更がプロセスの終了後も保持されるよう、変更操作をディスク上の永続的なアウトボックス(Outbox)キューに追加します。iOSの日和見的かつ非決定論的なバックグラウンド実行を制御するため、バックグラウンド処理は以下の階層構造で構成します。 1. バックグラウンド移行時のアウトボックスフラッシュ: `UIApplication.shared.beginBackgroundTask(expirationHandler:)` を利用して開始し、実行中の通信や短時間で完了する保留中の変更を処理します。 2. 定期的なスケジュール同期: `BGTaskScheduler` に登録し、軽量なメタデータや差分同期には `BGAppRefreshTask` を、重い同期処理には `BGProcessingTask` を使い分けます。 3. 大規模アセットの同期: ファイルのアップロード/ダウンロードをバックグラウンド構成の `URLSessionConfiguration` を用いてプロセス外(out-of-process)で処理します。 4. サーバー起点の同期: サイレントプッシュ通知(`content-available: 1`)を利用した適宜の起床処理を行います。 バックグラウンドタスクは任意のタイミングで中断または再スケジュールされる可能性があるため、データの重複なしに安全に再試行できるよう、クライアント側で生成したべき等性キー(例: UUID)を使用する必要があります。タスクの `expirationHandler` が呼び出された場合は、実行中の処理を直ちにキャンセルし、未確定の操作を保留状態に戻した上で `setTaskCompleted(success: false)` を呼び出します。最終的な整合性(Eventual Consistency)は、CRDT、バージョンベクトル、または削除トゥームストーン(tombstone)を伴うLast-Write-Wins(LWW)などの競合解決メカニズムによって維持し、リモートの差分がコミットされていないローカルのアウトボックス内の変更を上書きしないように保証します。

import BackgroundTasks
import Foundation

final class NoteSyncCoordinator {
    static let shared = NoteSyncCoordinator()
    private let syncTaskID = "com.notesapp.sync.refresh"

    func registerSyncTask() {
        BGTaskScheduler.shared.register(forTaskWithIdentifier: syncTaskID, using: nil) { task in
            guard let refreshTask = task as? BGAppRefreshTask else { return }
            self.handleAppRefresh(task: refreshTask)
        }
    }

    func scheduleNextSync() {
        let request = BGAppRefreshTaskRequest(identifier: syncTaskID)
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        try? BGTaskScheduler.shared.submit(request)
    }

    private func handleAppRefresh(task: BGAppRefreshTask) {
        scheduleNextSync()
        let syncTask = Task {
            do {
                try await SyncEngine.shared.flushPersistentOutboxAndFetchDeltas()
                task.setTaskCompleted(success: true)
            } catch {
                task.setTaskCompleted(success: false)
            }
        }

        task.expirationHandler = {
            syncTask.cancel()
        }
    }
}
AI コーチを使ってこの質問に答えてみる