Colloquio StoreKit 2: Gestione Abbonamenti e Validazione Ricevute
Padroneggia le domande di colloquio iOS su StoreKit 2, gestione abbonamenti, validazione ricevute e implementazione degli acquisti in-app con esempi pratici di codice Swift.

StoreKit 2 rappresenta un cambiamento fondamentale nel modo in cui vengono gestiti gli acquisti in-app su iOS. Introdotto con iOS 15, questo framework moderno semplifica drasticamente il codice necessario per la gestione degli abbonamenti e la validazione delle transazioni. Con iOS 18, StoreKit 1 è ufficialmente deprecato, rendendo StoreKit 2 l'unica via supportata. I colloqui tecnici iOS affrontano regolarmente questo argomento per valutare l'esperienza degli sviluppatori nella monetizzazione delle app.
StoreKit 2 sfrutta un'API Swift nativa con async/await, eliminando la necessità di callback complessi e validazione delle ricevute lato client. Gli intervistatori apprezzano i candidati che sanno articolare le differenze fondamentali con StoreKit 1 e le nuove funzionalità iOS 18.4 come appTransactionID e SubscriptionOfferView.
Panoramica dell'Architettura StoreKit 2
StoreKit 2 è costruito su un'architettura moderna che sfrutta appieno Swift Concurrency. A differenza di StoreKit 1, tutte le operazioni sono asincrone e utilizzano tipi nativi di Swift.
Recupero dei Prodotti Disponibili
Il primo passo consiste nel caricare i prodotti configurati su App Store Connect. StoreKit 2 semplifica questa operazione con un'API dichiarativa.
import StoreKit
actor ProductService {
// Product identifiers configured in App Store Connect
private let productIdentifiers: Set<String> = [
"com.app.subscription.monthly",
"com.app.subscription.yearly",
"com.app.premium.lifetime"
]
// Product cache to avoid repeated network calls
private var cachedProducts: [Product] = []
func loadProducts() async throws -> [Product] {
// Return cache if available
guard cachedProducts.isEmpty else {
return cachedProducts
}
// Asynchronous request to the 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 }
}
}L'utilizzo di un actor garantisce la thread-safety nella gestione della cache dei prodotti.
Tipi di Prodotti in StoreKit 2
StoreKit 2 distingue chiaramente tra i diversi tipi di prodotti. Comprendere questa distinzione è essenziale per i colloqui.
import StoreKit
extension Product.ProductType {
var displayName: String {
switch self {
case .consumable:
// Consumable products (credits, lives, etc.)
return "Consumable"
case .nonConsumable:
// Permanent purchases (feature unlock)
return "Non-Consumable"
case .autoRenewable:
// Subscriptions with automatic renewal
return "Auto-Renewable Subscription"
case .nonRenewable:
// Subscriptions without renewal (temporary pass)
return "Non-Renewable Subscription"
@unknown default:
return "Unknown"
}
}
}Gli abbonamenti con rinnovo automatico rappresentano il modello di business dominante per le applicazioni iOS moderne.
Gestione delle Transazioni di Acquisto
Il processo di acquisto in StoreKit 2 diventa lineare grazie ad async/await. Niente più delegate e callback annidati di StoreKit 1.
Avvio di un Acquisto
A partire da iOS 18.2, gli acquisti richiedono un parametro di contesto UI. Questa modifica garantisce che il foglio di acquisto appaia correttamente nella gerarchia delle view dell'app.
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 {
// Initiate purchase with UI context (iOS 18.2+)
let result = try await product.purchase(confirmIn: scene)
switch result {
case .success(let verification):
// Verify the transaction signature
let transaction = try checkVerification(verification)
// Mark the transaction as finished
await transaction.finish()
return transaction
case .userCancelled:
// User cancelled the purchase modal
throw PurchaseError.purchaseCancelled
case .pending:
// Purchase pending (parental approval, etc.)
throw PurchaseError.purchasePending
@unknown default:
throw PurchaseError.unknown
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
// Transaction signed and verified by the App Store
return safe
case .unverified(_, let error):
// Invalid or corrupted signature
throw PurchaseError.verificationFailed
}
}
}Il metodo finish() è cruciale: segnala all'App Store che il contenuto è stato consegnato all'utente.
Non dimenticare mai di chiamare transaction.finish() dopo aver consegnato il contenuto. Una transazione non finalizzata verrà ripristinata al successivo avvio dell'app, causando comportamenti imprevisti.
Ascolto delle Transazioni in Background
StoreKit 2 introduce Transaction.updates, uno stream asincrono che emette le transazioni che avvengono al di fuori dell'app (rinnovi, acquisti family sharing, ecc.).
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
func startObserving() {
// Cancel any previous observation
updateTask?.cancel()
updateTask = Task(priority: .background) {
// Infinite stream of transaction updates
for await result in Transaction.updates {
do {
let transaction = try self.checkVerification(result)
// Process the transaction based on its type
await self.handleTransaction(transaction)
// Always finish the transaction
await transaction.finish()
} catch {
// Log the error for debugging
print("Transaction verification failed: (error)")
}
}
}
}
func stopObserving() {
updateTask?.cancel()
updateTask = nil
}
private func handleTransaction(_ transaction: Transaction) async {
switch transaction.productType {
case .autoRenewable:
// Update subscription status
await updateSubscriptionStatus(transaction)
case .nonConsumable:
// Unlock premium feature
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 {
// Implementation of status update
}
private func unlockFeature(_ productID: String) async {
// Implementation of feature unlock
}
enum PurchaseError: Error {
case verificationFailed
}
}L'osservazione delle transazioni dovrebbe iniziare non appena l'app viene avviata per evitare di perdere aggiornamenti.
Domande Comuni di Colloquio sugli Abbonamenti StoreKit 2
I colloqui iOS includono spesso domande specifiche sulla gestione degli abbonamenti. Ecco le più comuni con risposte dettagliate. Per ulteriori argomenti di colloquio Swift, consulta la guida su Swift Structured Concurrency.
Domanda 1: Come Verificare lo Stato Attuale dell'Abbonamento?
StoreKit 2 fornisce accesso diretto agli entitlement attivi. Si noti che Transaction.currentEntitlement(for:) è deprecato da iOS 18.4. Il nuovo currentEntitlements(for:) restituisce una sequenza asincrona per gestire entitlement multipli (ad esempio, abbonamento più accesso Family Sharing).
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 {
// Use the new 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
)
}
// Check if the subscription is still active
let isActive = transaction.expirationDate ?? Date.distantPast > Date()
// Retrieve renewal information
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
}
// Retrieve renewal status
let status = try? await subscription.status.first
return status?.renewalInfo
}
}Questo approccio fornisce lo stato preciso dell'abbonamento senza chiamate al server. Il campo appTransactionID, introdotto in iOS 18.4, consente il tracciamento dei clienti attraverso le transazioni senza chiamate server-to-server.
Pronto a superare i tuoi colloqui su iOS?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Domanda 2: Come Gestire Periodi di Prova Gratuiti e Offerte Promozionali?
StoreKit 2 semplifica l'accesso alle informazioni sulle offerte. Gli intervistatori spesso testano la comprensione dei diversi tipi di offerte. Da iOS 18.4, le offerte promozionali possono essere firmate usando JWS con il nuovo PromotionOfferV2SignatureCreator disponibile nella App Store Server Library.
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+: offer codes for all product types
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:
// Free trial period
return .freeTrial(duration: formatPeriod(offer.period))
case .payAsYouGo:
// Reduced price over multiple periods
return .payAsYouGo(
periods: offer.periodCount,
price: offer.price
)
case .payUpFront:
// Single payment for a period
return .payUpFront(
duration: formatPeriod(offer.period),
price: offer.price
)
case .oneTime:
// One-time offer code (consumables, non-consumables, non-renewing)
return .oneTime(price: offer.price)
@unknown default:
return .none
}
}
static func checkEligibility(for product: Product) async -> Bool {
// Check if user can benefit from the offer
guard let subscription = product.subscription else {
return false
}
// isEligibleForIntroOffer indicates if this is a new subscriber
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 ""
}
}
}Il metodo isEligibleForIntroOffer è essenziale per evitare di mostrare offerte non disponibili. Si noti che i codici offerta, precedentemente limitati agli abbonamenti con rinnovo automatico, ora supportano consumabili, non consumabili e abbonamenti senza rinnovo da iOS 16.3.
Domanda 3: Come Ripristinare gli Acquisti Precedenti?
Il ripristino degli acquisti è una funzionalità obbligatoria secondo le App Store Review Guidelines.
import StoreKit
actor RestoreManager {
struct RestoreResult {
let restoredProducts: [String]
let activeSubscriptions: [String]
}
func restorePurchases() async throws -> RestoreResult {
var restoredProducts: [String] = []
var activeSubscriptions: [String] = []
// Synchronize with the App Store
try await AppStore.sync()
// Iterate through all user transactions
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
switch transaction.productType {
case .nonConsumable:
// Restore permanent purchases
restoredProducts.append(transaction.productID)
case .autoRenewable:
// Check if subscription is active
if let expiration = transaction.expirationDate,
expiration > Date() {
activeSubscriptions.append(transaction.productID)
}
default:
// Consumables are not restorable
break
}
}
return RestoreResult(
restoredProducts: restoredProducts,
activeSubscriptions: activeSubscriptions
)
}
}AppStore.sync() forza la sincronizzazione con i server Apple, utile dopo una reinstallazione.
SubscriptionOfferView: Merchandising Moderno degli Abbonamenti
iOS 18.4 introduce SubscriptionOfferView, un componente SwiftUI che visualizza le opzioni di abbonamento con caricamento automatico dei metadati dei prodotti. Questo è un argomento comune nei colloqui per sviluppatori che creano app basate su abbonamenti.
import SwiftUI
import StoreKit
struct SubscriptionSettingsView: View {
let subscriptionGroupID = "com.app.subscription.group"
var body: some View {
VStack(spacing: 16) {
// Show upgrade options for current subscribers
SubscriptionOfferView(groupID: subscriptionGroupID)
.visibleRelationship(.upgrade)
.prefersPromotionalIcon(true)
.subscriptionOfferViewDetailAction { path in
path.append("subscriptionDetail")
}
// Show current plan details
SubscriptionOfferView(groupID: subscriptionGroupID)
.visibleRelationship(.current)
}
.padding()
}
}Il parametro visibleRelationship controlla quali livelli di abbonamento vengono visualizzati: .upgrade mostra i piani di un livello superiore, .downgrade mostra i livelli inferiori, .crossgrade mostra livelli equivalenti con miglior valore, e .all visualizza ogni piano nel gruppo.
Validazione delle Ricevute Lato Server
La validazione lato server rimane consigliata per le applicazioni con abbonamenti critici. StoreKit 2 utilizza JWS (JSON Web Signatures) per una validazione moderna. L'endpoint deprecato verifyReceipt è stato sostituito dalla App Store Server API.
Estrazione dei Dati di Transazione per il Server
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 {
// Create payload for the server
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) }
)
// Encode to JSON
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(payload)
// Send to server
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
}
// Decode server response
let validationResponse = try JSONDecoder().decode(
ValidationResponse.self,
from: data
)
return validationResponse.isValid
}
}
struct ValidationResponse: Codable {
let isValid: Bool
let subscriptionStatus: String?
}La jwsRepresentation contiene la firma crittografica verificabile dal server. Il nuovo appTransactionID consente un tracciamento preciso dei clienti senza ulteriori round-trip al server.
L'App Store Server API di Apple fornisce validazione lato server con token JWS, Server Notifications V2 per eventi di abbonamento in tempo reale, e la nuova SubscriptionStatus API che accetta Transaction ID (iOS 18.4+) per interrogare direttamente lo stato.
Configurazione delle Notifiche del Server
Le Server Notifications V2 consentono la ricezione di eventi in tempo reale.
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?
}
}Queste notifiche consentono di mantenere lo stato degli abbonamenti sincronizzato con il backend.
Gestione degli Errori e Casi Limite
I colloqui spesso testano la comprensione degli scenari di errore e della loro corretta gestione. Per ulteriori informazioni sui pattern architetturali iOS, consulta la guida ai colloqui iOS senior sui design pattern.
Errori Comuni in 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 {
// Check if it's a StoreKit error
if let storeKitError = error as? StoreKitError {
switch storeKitError {
case .networkError:
return .networkError
case .userCancelled:
// Don't show error, user cancelled
return .unknown
case .notAvailableInStorefront:
return .productNotAvailable
case .notEntitled:
return .notAuthorized
default:
return .unknown
}
}
// Purchase errors
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
}
}Una gestione chiara degli errori migliora l'esperienza utente e semplifica il debug.
Supporto alla Modalità Offline
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
}
}
// Save locally
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 [:]
}
// Filter expired entitlements
let now = Date()
return entitlements.filter { _, expiration in
expiration > now
}
}
func hasValidEntitlement(for productID: String) -> Bool {
let cached = getCachedEntitlements()
return cached[productID] != nil
}
}La cache locale mantiene l'accesso alle funzionalità premium anche senza connettività.
Best Practice per la Produzione
Architettura Consigliata
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() {
// Start transaction observation
startTransactionObserver()
// Load initial state
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()
}
}Questa architettura centralizzata semplifica la gestione dello stato degli acquisti in tutta l'applicazione.
Fonti
- What's new in StoreKit and In-App Purchase, WWDC25 - Aggiunte API iOS 18.4 inclusi
appTransactionIDeSubscriptionOfferView - App Store Server API Documentation - Validazione lato server con JWS e Server Notifications V2
- StoreKit Documentation - Riferimento API completo per StoreKit 2
Punti Chiave per i Colloqui su StoreKit 2
StoreKit 2 trasforma la gestione degli acquisti in-app in un'esperienza di sviluppo moderna e sicura. Punti chiave da ricordare per un colloquio:
- L'API nativa async/await elimina i delegate e callback complessi di StoreKit 1, che è ora deprecato da iOS 18
- La verifica automatica tramite
VerificationResultgestisce la validazione crittografica delle transazioni Transaction.updatesfornisce uno stream asincrono per le transazioni in background inclusi rinnovi e family sharingTransaction.currentEntitlements(for:)sostituisce il deprecatocurrentEntitlement(for:)per gestire entitlement multipli- Il campo
appTransactionID(iOS 18.4+) consente il tracciamento dei clienti senza chiamate server-to-server SubscriptionOfferViewfornisce UI integrata per il merchandising degli abbonamenti convisibleRelationshipper opzioni di upgrade, downgrade e crossgrade- Le offerte promozionali possono essere firmate usando JWS con
PromotionOfferV2SignatureCreator - I codici offerta ora supportano consumabili, non consumabili e abbonamenti senza rinnovo (iOS 16.3+)
- La validazione server utilizza l'App Store Server API con token JWS, sostituendo l'endpoint deprecato
verifyReceipt - Chiamare sempre
transaction.finish()dopo la consegna del contenuto per prevenire comportamenti di ripristino inattesi - Implementare il caching offline degli entitlement per un'esperienza utente fluida senza connettività
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Sapresti trovare il bug in iOS?
Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Scritto da
Anthony Fillion-MailletFondatore di SharpSkill
Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.
Aggiornato il 20 ottobre 2026
Tag
Condividi
Articoli correlati

Swift Testing Framework Colloquio 2026: Macro #expect e #require vs XCTest
Padroneggia il nuovo Swift Testing Framework per i colloqui iOS: macro #expect e #require, migrazione da XCTest, pattern avanzati ed errori comuni.

Colloquio iOS Push Notifications 2026: APNs, token e troubleshooting
Guida completa per preparare i colloqui iOS su Push Notifications, APNs, gestione dei token e troubleshooting. Domande frequenti con risposte dettagliate.

Colloquio iOS Senior 2026: Domande su Architettura e Design Pattern
Preparazione ai colloqui iOS senior con domande chiave su MVVM, VIPER, Clean Architecture e design pattern. Guida completa con esempi di codice Swift.