StoreKit 2面接対策:サブスクリプション管理とレシート検証
StoreKit 2、サブスクリプション管理、レシート検証、アプリ内課金実装に関するiOS面接の質問を、実用的なSwiftコード例とともにマスターしましょう。

StoreKit 2は、iOSにおけるアプリ内課金の処理方法における根本的な変革を表します。iOS 15で導入されたこの最新フレームワークは、サブスクリプション管理とトランザクション検証に必要なコードを大幅に簡素化します。iOS 18でStoreKit 1は正式に非推奨となり、StoreKit 2が唯一のサポート対象となりました。iOSの技術面接では、アプリ収益化に関する開発者の専門知識を評価するため、このトピックが定期的に取り上げられます。
StoreKit 2は、async/awaitを使用したネイティブSwift APIを活用し、複雑なコールバックやクライアントサイドのレシート検証を不要にします。面接官は、StoreKit 1との根本的な違いや、appTransactionIDやSubscriptionOfferViewなどのiOS 18.4の新機能を明確に説明できる候補者を高く評価します。
StoreKit 2アーキテクチャの概要
StoreKit 2は、Swift Concurrencyを最大限に活用するモダンなアーキテクチャの上に構築されています。StoreKit 1とは異なり、すべての操作は非同期であり、ネイティブSwift型を使用します。
利用可能な製品の取得
最初のステップは、App Store Connectで設定された製品を読み込むことです。StoreKit 2は宣言的なAPIでこの操作を簡素化します。
import StoreKit
actor ProductService {
// App Store Connectで設定された製品識別子
private let productIdentifiers: Set<String> = [
"com.app.subscription.monthly",
"com.app.subscription.yearly",
"com.app.premium.lifetime"
]
// 繰り返しのネットワーク呼び出しを避けるための製品キャッシュ
private var cachedProducts: [Product] = []
func loadProducts() async throws -> [Product] {
// キャッシュが利用可能な場合は返す
guard cachedProducts.isEmpty else {
return cachedProducts
}
// App Storeへの非同期リクエスト
let products = try await Product.products(for: productIdentifiers)
cachedProducts = products
return products
}
func product(for identifier: String) async throws -> Product? {
let products = try await loadProducts()
return products.first { $0.id == identifier }
}
}actorを使用することで、製品キャッシュの管理時にスレッド安全性が保証されます。
StoreKit 2の製品タイプ
StoreKit 2は、異なる製品タイプを明確に区別します。この区別を理解することは面接で重要です。
import StoreKit
extension Product.ProductType {
var displayName: String {
switch self {
case .consumable:
// 消耗型製品(クレジット、ライフなど)
return "Consumable"
case .nonConsumable:
// 永続的な購入(機能のアンロック)
return "Non-Consumable"
case .autoRenewable:
// 自動更新サブスクリプション
return "Auto-Renewable Subscription"
case .nonRenewable:
// 更新なしのサブスクリプション(一時的なパス)
return "Non-Renewable Subscription"
@unknown default:
return "Unknown"
}
}
}自動更新サブスクリプションは、現代のiOSアプリケーションにとって主流のビジネスモデルです。
購入トランザクションの処理
StoreKit 2における購入プロセスは、async/awaitのおかげで線形的になります。StoreKit 1のデリゲートやネストされたコールバックはもう必要ありません。
購入の開始
iOS 18.2以降、購入にはUIコンテキストパラメータが必要です。この変更により、購入シートがアプリのビュー階層内で正しく表示されます。
import StoreKit
import UIKit
actor PurchaseManager {
enum PurchaseError: Error {
case productNotFound
case purchaseCancelled
case purchasePending
case verificationFailed
case unknown
}
func purchase(_ product: Product, in scene: UIWindowScene) async throws -> Transaction {
// UIコンテキスト付きで購入を開始(iOS 18.2以降)
let result = try await product.purchase(confirmIn: scene)
switch result {
case .success(let verification):
// トランザクション署名の検証
let transaction = try checkVerification(verification)
// トランザクションを完了としてマーク
await transaction.finish()
return transaction
case .userCancelled:
// ユーザーが購入モーダルをキャンセル
throw PurchaseError.purchaseCancelled
case .pending:
// 保留中の購入(保護者の承認など)
throw PurchaseError.purchasePending
@unknown default:
throw PurchaseError.unknown
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
// App Storeによって署名・検証されたトランザクション
return safe
case .unverified(_, let error):
// 無効または破損した署名
throw PurchaseError.verificationFailed
}
}
}finish()メソッドは重要です:コンテンツがユーザーに配信されたことをApp Storeに通知します。
コンテンツ配信後にtransaction.finish()を呼び出すことを決して忘れないでください。完了していないトランザクションは次のアプリ起動時に復元され、予期しない動作を引き起こします。
バックグラウンドトランザクションの監視
StoreKit 2はTransaction.updatesを導入します。これは、アプリの外で発生するトランザクション(更新、ファミリー共有購入など)を発行する非同期ストリームです。
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
func startObserving() {
// 以前の監視をキャンセル
updateTask?.cancel()
updateTask = Task(priority: .background) {
// トランザクション更新の無限ストリーム
for await result in Transaction.updates {
do {
let transaction = try self.checkVerification(result)
// タイプに応じてトランザクションを処理
await self.handleTransaction(transaction)
// 常にトランザクションを完了
await transaction.finish()
} catch {
// デバッグのためにエラーをログ記録
print("Transaction verification failed: (error)")
}
}
}
}
func stopObserving() {
updateTask?.cancel()
updateTask = nil
}
private func handleTransaction(_ transaction: Transaction) async {
switch transaction.productType {
case .autoRenewable:
// サブスクリプションステータスを更新
await updateSubscriptionStatus(transaction)
case .nonConsumable:
// プレミアム機能をアンロック
await unlockFeature(transaction.productID)
default:
break
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
return safe
case .unverified:
throw PurchaseError.verificationFailed
}
}
private func updateSubscriptionStatus(_ transaction: Transaction) async {
// ステータス更新の実装
}
private func unlockFeature(_ productID: String) async {
// 機能アンロックの実装
}
enum PurchaseError: Error {
case verificationFailed
}
}トランザクションの監視は、更新を見逃さないようにアプリ起動と同時に開始する必要があります。
StoreKit 2サブスクリプションに関するよくある面接質問
iOS面接では、サブスクリプション管理に関する具体的な質問がよく含まれます。以下は最も一般的なものと、その詳細な回答です。その他のSwift面接トピックについては、Swift構造化並行処理ガイドを参照してください。
質問1:現在のサブスクリプションステータスを確認する方法は?
StoreKit 2は、アクティブなエンタイトルメントへの直接アクセスを提供します。Transaction.currentEntitlement(for:)はiOS 18.4で非推奨となりました。新しいcurrentEntitlements(for:)は、複数のエンタイトルメント(例:サブスクリプションとファミリー共有アクセス)を処理するための非同期シーケンスを返します。
import StoreKit
struct SubscriptionStatusChecker {
struct SubscriptionStatus {
let isActive: Bool
let expirationDate: Date?
let willRenew: Bool
let productID: String?
let appTransactionID: UInt64? // iOS 18.4以降
}
static func checkCurrentStatus(for productID: String) async -> SubscriptionStatus {
// 新しいcurrentEntitlements(for:) APIを使用(iOS 18.4以降)
var latestTransaction: Transaction?
for await result in Transaction.currentEntitlements(for: productID) {
if case .verified(let transaction) = result {
if transaction.productType == .autoRenewable {
if latestTransaction == nil ||
transaction.purchaseDate > latestTransaction!.purchaseDate {
latestTransaction = transaction
}
}
}
}
guard let transaction = latestTransaction else {
return SubscriptionStatus(
isActive: false,
expirationDate: nil,
willRenew: false,
productID: nil,
appTransactionID: nil
)
}
// サブスクリプションがまだアクティブかチェック
let isActive = transaction.expirationDate ?? Date.distantPast > Date()
// 更新情報を取得
let renewalInfo = await getRenewalInfo(for: transaction.productID)
return SubscriptionStatus(
isActive: isActive,
expirationDate: transaction.expirationDate,
willRenew: renewalInfo?.willAutoRenew ?? false,
productID: transaction.productID,
appTransactionID: transaction.appTransactionID
)
}
private static func getRenewalInfo(for productID: String) async -> Product.SubscriptionInfo.RenewalInfo? {
guard let product = try? await Product.products(for: [productID]).first,
let subscription = product.subscription else {
return nil
}
// 更新ステータスを取得
let status = try? await subscription.status.first
return status?.renewalInfo
}
}このアプローチは、サーバー呼び出しなしで正確なサブスクリプション状態を提供します。iOS 18.4で導入されたappTransactionIDフィールドにより、サーバー間通信なしでトランザクション間で顧客を追跡できます。
iOSの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
質問2:無料トライアルとプロモーションオファーの処理方法は?
StoreKit 2はオファー情報へのアクセスを簡素化します。面接官はしばしば異なるオファータイプの理解をテストします。iOS 18.4以降、App Store Server Libraryで利用可能な新しいPromotionOfferV2SignatureCreatorを使用して、JWSでプロモーションオファーに署名できます。
import StoreKit
struct OfferManager {
enum OfferType {
case freeTrial(duration: String)
case payAsYouGo(periods: Int, price: Decimal)
case payUpFront(duration: String, price: Decimal)
case oneTime(price: Decimal) // iOS 17.2以降:すべての製品タイプのオファーコード
case none
}
static func getIntroductoryOffer(for product: Product) -> OfferType {
guard let subscription = product.subscription,
let offer = subscription.introductoryOffer else {
return .none
}
switch offer.paymentMode {
case .freeTrial:
// 無料トライアル期間
return .freeTrial(duration: formatPeriod(offer.period))
case .payAsYouGo:
// 複数期間にわたる割引価格
return .payAsYouGo(
periods: offer.periodCount,
price: offer.price
)
case .payUpFront:
// 期間に対する一括支払い
return .payUpFront(
duration: formatPeriod(offer.period),
price: offer.price
)
case .oneTime:
// 一回限りのオファーコード(消耗型、非消耗型、非更新型)
return .oneTime(price: offer.price)
@unknown default:
return .none
}
}
static func checkEligibility(for product: Product) async -> Bool {
// ユーザーがオファーを利用できるかチェック
guard let subscription = product.subscription else {
return false
}
// isEligibleForIntroOfferは新規サブスクライバーかを示す
return await subscription.isEligibleForIntroOffer
}
private static func formatPeriod(_ period: Product.SubscriptionPeriod) -> String {
let value = period.value
switch period.unit {
case .day:
return "(value) day(value > 1 ? "s" : "")"
case .week:
return "(value) week(value > 1 ? "s" : "")"
case .month:
return "(value) month(value > 1 ? "s" : "")"
case .year:
return "(value) year(value > 1 ? "s" : "")"
@unknown default:
return ""
}
}
}isEligibleForIntroOfferメソッドは、利用できないオファーの表示を回避するために重要です。以前は自動更新サブスクリプションに限定されていたオファーコードは、iOS 16.3以降、消耗型、非消耗型、非更新型サブスクリプションもサポートしています。
質問3:過去の購入を復元する方法は?
購入の復元は、App Storeレビューガイドラインに従って必須の機能です。
import StoreKit
actor RestoreManager {
struct RestoreResult {
let restoredProducts: [String]
let activeSubscriptions: [String]
}
func restorePurchases() async throws -> RestoreResult {
var restoredProducts: [String] = []
var activeSubscriptions: [String] = []
// App Storeと同期
try await AppStore.sync()
// すべてのユーザートランザクションを反復
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
switch transaction.productType {
case .nonConsumable:
// 永続的な購入を復元
restoredProducts.append(transaction.productID)
case .autoRenewable:
// サブスクリプションがアクティブかチェック
if let expiration = transaction.expirationDate,
expiration > Date() {
activeSubscriptions.append(transaction.productID)
}
default:
// 消耗型は復元不可
break
}
}
return RestoreResult(
restoredProducts: restoredProducts,
activeSubscriptions: activeSubscriptions
)
}
}AppStore.sync()はAppleサーバーとの同期を強制し、再インストール後に役立ちます。
SubscriptionOfferView:モダンなサブスクリプションマーチャンダイジング
iOS 18.4で導入されたSubscriptionOfferViewは、製品メタデータの自動読み込みを備えたサブスクリプションオプションを表示するSwiftUIコンポーネントです。サブスクリプションベースのアプリを構築する開発者にとって、よくある面接トピックです。
import SwiftUI
import StoreKit
struct SubscriptionSettingsView: View {
let subscriptionGroupID = "com.app.subscription.group"
var body: some View {
VStack(spacing: 16) {
// 現在の購読者にアップグレードオプションを表示
SubscriptionOfferView(groupID: subscriptionGroupID)
.visibleRelationship(.upgrade)
.prefersPromotionalIcon(true)
.subscriptionOfferViewDetailAction { path in
path.append("subscriptionDetail")
}
// 現在のプラン詳細を表示
SubscriptionOfferView(groupID: subscriptionGroupID)
.visibleRelationship(.current)
}
.padding()
}
}visibleRelationshipパラメータは、表示されるサブスクリプションティアを制御します:.upgradeは1つ上のティアのプランを表示し、.downgradeは下のティアを、.crossgradeはより価値のある同等ティアを、.allはグループ内のすべてのプランを表示します。
サーバーサイドのレシート検証
サーバーサイド検証は、重要なサブスクリプションを持つアプリケーションには引き続き推奨されます。StoreKit 2は、最新の検証のためにJWS (JSON Web Signatures) を使用します。非推奨のverifyReceiptエンドポイントは、App Store Server APIに置き換えられました。
サーバー用のトランザクションデータの抽出
import StoreKit
struct TransactionPayload: Codable {
let transactionID: String
let originalTransactionID: String
let productID: String
let purchaseDate: Date
let expirationDate: Date?
let jwsRepresentation: String
let appTransactionID: String? // iOS 18.4以降
}
actor ServerValidationService {
private let apiEndpoint = "https://api.example.com/verify"
func validateWithServer(_ transaction: Transaction) async throws -> Bool {
// サーバー用のペイロードを作成
let payload = TransactionPayload(
transactionID: String(transaction.id),
originalTransactionID: String(transaction.originalID),
productID: transaction.productID,
purchaseDate: transaction.purchaseDate,
expirationDate: transaction.expirationDate,
jwsRepresentation: transaction.jwsRepresentation ?? "",
appTransactionID: transaction.appTransactionID.map { String($0) }
)
// JSONにエンコード
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(payload)
// サーバーへ送信
var request = URLRequest(url: URL(string: apiEndpoint)!)
request.httpMethod = "POST"
request.httpBody = jsonData
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
return false
}
// サーバー応答をデコード
let validationResponse = try JSONDecoder().decode(
ValidationResponse.self,
from: data
)
return validationResponse.isValid
}
}
struct ValidationResponse: Codable {
let isValid: Bool
let subscriptionStatus: String?
}jwsRepresentationにはサーバーで検証可能な暗号化署名が含まれています。新しいappTransactionIDにより、追加のサーバーラウンドトリップなしで正確な顧客追跡が可能です。
AppleのApp Store Server APIは、JWSトークンによるサーバーサイド検証、リアルタイムサブスクリプションイベント用のServer Notifications V2、そしてステータスを直接クエリするためのトランザクションIDを受け入れる新しいSubscriptionStatus API(iOS 18.4以降)を提供します。
サーバー通知の設定
Server Notifications V2はリアルタイムイベントの受信を可能にします。
enum NotificationType: String, Codable {
case subscribed = "SUBSCRIBED"
case didRenew = "DID_RENEW"
case didFailToRenew = "DID_FAIL_TO_RENEW"
case didChangeRenewalStatus = "DID_CHANGE_RENEWAL_STATUS"
case expired = "EXPIRED"
case gracePeriodExpired = "GRACE_PERIOD_EXPIRED"
case offerRedeemed = "OFFER_REDEEMED"
case priceIncrease = "PRICE_INCREASE"
case refund = "REFUND"
case renewalExtended = "RENEWAL_EXTENDED"
case revoke = "REVOKE"
}
struct ServerNotification: Codable {
let notificationType: NotificationType
let subtype: String?
let data: NotificationData
struct NotificationData: Codable {
let bundleId: String
let environment: String
let signedTransactionInfo: String
let signedRenewalInfo: String?
}
}これらの通知により、サブスクリプション状態をバックエンドと同期して保つことができます。
エラー処理とエッジケース
面接ではしばしば、エラーシナリオの理解とその適切な処理がテストされます。iOSアーキテクチャパターンの詳細については、iOSシニア面接アーキテクチャガイドを参照してください。
StoreKit 2の一般的なエラー
import StoreKit
struct PurchaseErrorHandler {
enum UserFacingError {
case networkError
case paymentFailed
case notAuthorized
case productNotAvailable
case unknown
var message: String {
switch self {
case .networkError:
return "Connection failed. Please check your internet connection."
case .paymentFailed:
return "Payment failed. Please verify your payment information."
case .notAuthorized:
return "In-app purchases are disabled on this device."
case .productNotAvailable:
return "This product is not currently available."
case .unknown:
return "An unexpected error occurred."
}
}
}
static func handle(_ error: Error) -> UserFacingError {
// StoreKitエラーかチェック
if let storeKitError = error as? StoreKitError {
switch storeKitError {
case .networkError:
return .networkError
case .userCancelled:
// エラーを表示しない、ユーザーがキャンセル
return .unknown
case .notAvailableInStorefront:
return .productNotAvailable
case .notEntitled:
return .notAuthorized
default:
return .unknown
}
}
// 購入エラー
if let purchaseError = error as? Product.PurchaseError {
switch purchaseError {
case .invalidQuantity:
return .unknown
case .productUnavailable:
return .productNotAvailable
case .purchaseNotAllowed:
return .notAuthorized
default:
return .unknown
}
}
return .unknown
}
}明確なエラー処理は、ユーザー体験を向上させ、デバッグを簡素化します。
オフラインモードのサポート
import StoreKit
actor OfflineCapabilityManager {
private let userDefaults = UserDefaults.standard
private let entitlementsKey = "cached_entitlements"
func cacheCurrentEntitlements() async {
var entitlements: [String: Date] = [:]
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
entitlements[transaction.productID] = transaction.expirationDate
}
}
// ローカルに保存
if let data = try? JSONEncoder().encode(entitlements) {
userDefaults.set(data, forKey: entitlementsKey)
}
}
func getCachedEntitlements() -> [String: Date] {
guard let data = userDefaults.data(forKey: entitlementsKey),
let entitlements = try? JSONDecoder().decode(
[String: Date].self,
from: data
) else {
return [:]
}
// 期限切れのエンタイトルメントをフィルタ
let now = Date()
return entitlements.filter { _, expiration in
expiration > now
}
}
func hasValidEntitlement(for productID: String) -> Bool {
let cached = getCachedEntitlements()
return cached[productID] != nil
}
}ローカルキャッシングにより、接続がなくてもプレミアム機能へのアクセスが維持されます。
本番環境のベストプラクティス
推奨アーキテクチャ
import StoreKit
import Combine
@MainActor
final class SubscriptionService: ObservableObject {
static let shared = SubscriptionService()
@Published private(set) var products: [Product] = []
@Published private(set) var purchasedProductIDs: Set<String> = []
@Published private(set) var subscriptionStatus: SubscriptionStatus = .none
private var transactionObserver: Task<Void, Never>?
enum SubscriptionStatus {
case none
case active(expirationDate: Date)
case expired
case inGracePeriod
}
private init() {
// トランザクション監視を開始
startTransactionObserver()
// 初期状態をロード
Task {
await loadProducts()
await updateSubscriptionStatus()
}
}
private func startTransactionObserver() {
transactionObserver = Task.detached(priority: .background) {
for await result in Transaction.updates {
if case .verified(let transaction) = result {
await transaction.finish()
await self.updateSubscriptionStatus()
}
}
}
}
func loadProducts() async {
do {
let productIDs = [
"com.app.subscription.monthly",
"com.app.subscription.yearly"
]
products = try await Product.products(for: productIDs)
} catch {
print("Failed to load products: (error)")
}
}
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
await transaction.finish()
await updateSubscriptionStatus()
}
case .userCancelled:
break
case .pending:
break
@unknown default:
break
}
}
func updateSubscriptionStatus() async {
var purchasedIDs: Set<String> = []
var latestSubscription: Transaction?
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
purchasedIDs.insert(transaction.productID)
if transaction.productType == .autoRenewable {
if latestSubscription == nil ||
transaction.purchaseDate > latestSubscription!.purchaseDate {
latestSubscription = transaction
}
}
}
}
purchasedProductIDs = purchasedIDs
if let subscription = latestSubscription,
let expiration = subscription.expirationDate {
if expiration > Date() {
subscriptionStatus = .active(expirationDate: expiration)
} else {
subscriptionStatus = .expired
}
} else {
subscriptionStatus = .none
}
}
deinit {
transactionObserver?.cancel()
}
}この集中化されたアーキテクチャは、アプリケーション全体での購入状態管理を簡素化します。
Sources
- What's new in StoreKit and In-App Purchase, WWDC25 -
appTransactionIDとSubscriptionOfferViewを含むiOS 18.4 API追加 - App Store Server API Documentation - JWSとServer Notifications V2によるサーバーサイド検証
- StoreKit Documentation - StoreKit 2の完全なAPIリファレンス
StoreKit 2面接の重要ポイント
StoreKit 2は、アプリ内課金管理をモダンで安全な開発体験に変革します。面接で覚えておくべき重要なポイント:
- ネイティブasync/await APIにより、iOS 18で非推奨となったStoreKit 1の複雑なデリゲートやコールバックが不要に
VerificationResultによる自動検証がトランザクションの暗号化検証を処理Transaction.updatesは、更新やファミリー共有を含むバックグラウンドトランザクション用の非同期ストリームを提供Transaction.currentEntitlements(for:)は、複数のエンタイトルメントを処理するために非推奨のcurrentEntitlement(for:)を置き換えappTransactionIDフィールド(iOS 18.4以降)により、サーバー間通信なしで顧客追跡が可能SubscriptionOfferViewは、アップグレード、ダウングレード、クロスグレードオプション用のvisibleRelationshipを備えたサブスクリプションマーチャンダイジング用の組み込みUIを提供- プロモーションオファーは
PromotionOfferV2SignatureCreatorを使用してJWSで署名可能 - オファーコードは消耗型、非消耗型、非更新型サブスクリプションをサポート(iOS 16.3以降)
- サーバー検証は、非推奨の
verifyReceiptエンドポイントに代わり、JWSトークンを使用したApp Store Server APIを使用 - コンテンツ配信後は常に
transaction.finish()を呼び出し、予期しない復元動作を防止 - 接続がない状態でもシームレスなユーザー体験のため、オフラインエンタイトルメントキャッシングを実装
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
iOS のバグを見つけられますか
実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

執筆
Anthony Fillion-MailletSharpSkill 創業者
10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。
2026年10月20日 更新
タグ
共有
関連記事

Swift Testing Framework 面接 2026: #expect と #require マクロ vs XCTest
iOS面接のための新しいSwift Testing Frameworkを習得します: #expect と #require マクロ、XCTest からの移行、高度なパターン、よくある落とし穴。

iOSプッシュ通知 面接対策 2026: APNs・トークン・トラブルシューティング
Push Notifications、APNs、トークン管理、トラブルシューティングについてiOS面接を準備するための完全ガイドです。よくある質問に詳しい回答を添えています。

iOSシニア面接 2026:アーキテクチャとデザインパターンの質問集
MVVM、VIPER、Clean Architecture、デザインパターンに関する重要な質問でiOSシニア面接に備えます。Swiftコード例付きの完全ガイドです。