การสัมภาษณ์ StoreKit 2: การจัดการการสมัครสมาชิกและการตรวจสอบใบเสร็จ
เชี่ยวชาญคำถามสัมภาษณ์ iOS เกี่ยวกับ StoreKit 2 การจัดการการสมัครสมาชิก การตรวจสอบใบเสร็จ และการนำการซื้อในแอปไปใช้ พร้อมตัวอย่างโค้ด Swift ที่ใช้งานได้จริง

StoreKit 2 แสดงถึงการเปลี่ยนแปลงพื้นฐานในวิธีจัดการการซื้อในแอปบน iOS เปิดตัวพร้อมกับ iOS 15 framework ที่ทันสมัยนี้ช่วยลดความซับซ้อนของโค้ดที่จำเป็นสำหรับการจัดการการสมัครสมาชิกและการตรวจสอบธุรกรรมอย่างมาก ด้วย iOS 18, StoreKit 1 ถูก deprecated อย่างเป็นทางการ ทำให้ StoreKit 2 เป็นเส้นทางเดียวที่รองรับ การสัมภาษณ์ทางเทคนิค iOS มักครอบคลุมหัวข้อนี้เพื่อประเมินความเชี่ยวชาญของนักพัฒนาในการสร้างรายได้จากแอป
StoreKit 2 ใช้ประโยชน์จาก API Swift ดั้งเดิมด้วย async/await ทำให้ไม่จำเป็นต้องใช้ callback ที่ซับซ้อนและการตรวจสอบใบเสร็จฝั่งไคลเอนต์ ผู้สัมภาษณ์ให้ความสำคัญกับผู้สมัครที่สามารถอธิบายความแตกต่างพื้นฐานจาก StoreKit 1 ได้ และฟีเจอร์ใหม่ของ iOS 18.4 เช่น appTransactionID และ SubscriptionOfferView
ภาพรวมสถาปัตยกรรม StoreKit 2
StoreKit 2 ถูกสร้างขึ้นบนสถาปัตยกรรมที่ทันสมัยซึ่งใช้ประโยชน์จาก Swift Concurrency อย่างเต็มที่ ไม่เหมือน StoreKit 1 การดำเนินการทั้งหมดเป็นแบบอะซิงโครนัสและใช้ประเภท Swift ดั้งเดิม
การดึงผลิตภัณฑ์ที่มีอยู่
ขั้นตอนแรกเกี่ยวข้องกับการโหลดผลิตภัณฑ์ที่กำหนดค่าใน App Store Connect StoreKit 2 ทำให้การดำเนินการนี้ง่ายขึ้นด้วย API แบบประกาศ
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 }
}
}การใช้ actor รับประกันความปลอดภัยของเธรดเมื่อจัดการแคชผลิตภัณฑ์
ประเภทผลิตภัณฑ์ใน StoreKit 2
StoreKit 2 แยกแยะประเภทผลิตภัณฑ์ที่แตกต่างกันอย่างชัดเจน การเข้าใจความแตกต่างนี้เป็นสิ่งสำคัญสำหรับการสัมภาษณ์
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"
}
}
}การสมัครสมาชิกแบบต่ออายุอัตโนมัติเป็นรูปแบบธุรกิจหลักสำหรับแอปพลิเคชัน iOS ที่ทันสมัย
การจัดการธุรกรรมการซื้อ
กระบวนการซื้อใน StoreKit 2 กลายเป็นเชิงเส้นด้วย async/await ไม่มี delegate และ callback ซ้อนกันจาก StoreKit 1 อีกต่อไป
การเริ่มต้นการซื้อ
ตั้งแต่ iOS 18.2 เป็นต้นไป การซื้อต้องใช้พารามิเตอร์ UI context การเปลี่ยนแปลงนี้ทำให้แน่ใจว่า sheet การซื้อจะปรากฏอย่างถูกต้องในลำดับชั้น view ของแอป
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
}
}
}เมธอด finish() มีความสำคัญอย่างยิ่ง: มันแจ้ง App Store ว่าเนื้อหาได้ถูกส่งมอบให้ผู้ใช้แล้ว
อย่าลืมเรียก transaction.finish() หลังจากส่งมอบเนื้อหา ธุรกรรมที่ยังไม่เสร็จสิ้นจะถูกกู้คืนในการเปิดแอปครั้งถัดไป ทำให้เกิดพฤติกรรมที่ไม่คาดคิด
การฟังธุรกรรมเบื้องหลัง
StoreKit 2 แนะนำ Transaction.updates ซึ่งเป็นสตรีมอะซิงโครนัสที่ปล่อยธุรกรรมที่เกิดขึ้นภายนอกแอป (การต่ออายุ การซื้อ family sharing ฯลฯ)
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
}
}การสังเกตการณ์ธุรกรรมควรเริ่มทันทีที่แอปเปิด เพื่อหลีกเลี่ยงการพลาดอัปเดตใดๆ
คำถามสัมภาษณ์ทั่วไปเกี่ยวกับการสมัครสมาชิก StoreKit 2
การสัมภาษณ์ iOS มักรวมถึงคำถามเฉพาะเกี่ยวกับการจัดการการสมัครสมาชิก ต่อไปนี้เป็นคำถามที่พบบ่อยที่สุดพร้อมคำตอบโดยละเอียด สำหรับหัวข้อสัมภาษณ์ Swift เพิ่มเติม ดู คู่มือ Swift structured concurrency
คำถามที่ 1: จะตรวจสอบสถานะการสมัครสมาชิกปัจจุบันได้อย่างไร?
StoreKit 2 ให้การเข้าถึง entitlement ที่ใช้งานอยู่โดยตรง โปรดทราบว่า Transaction.currentEntitlement(for:) ถูก deprecated ตั้งแต่ iOS 18.4 API ใหม่ currentEntitlements(for:) ส่งคืน sequence อะซิงโครนัสเพื่อจัดการหลาย entitlement (เช่น การสมัครสมาชิกพร้อมการเข้าถึง 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
}
}วิธีการนี้ให้สถานะการสมัครสมาชิกที่ถูกต้องโดยไม่ต้องเรียกเซิร์ฟเวอร์ ฟิลด์ appTransactionID ที่เปิดตัวใน iOS 18.4 ช่วยให้ติดตามลูกค้าข้ามธุรกรรมได้โดยไม่ต้องเรียก server-to-server
พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
คำถามที่ 2: จะจัดการการทดลองใช้ฟรีและข้อเสนอโปรโมชันได้อย่างไร?
StoreKit 2 ทำให้การเข้าถึงข้อมูลข้อเสนอง่ายขึ้น ผู้สัมภาษณ์มักทดสอบความเข้าใจเกี่ยวกับประเภทข้อเสนอที่แตกต่างกัน ตั้งแต่ iOS 18.4 เป็นต้นไป ข้อเสนอโปรโมชันสามารถเซ็นได้โดยใช้ JWS กับ PromotionOfferV2SignatureCreator ใหม่ที่มีอยู่ใน 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 ""
}
}
}เมธอด isEligibleForIntroOffer มีความสำคัญในการหลีกเลี่ยงการแสดงข้อเสนอที่ไม่สามารถใช้งานได้ โปรดทราบว่า offer codes ที่ก่อนหน้านี้จำกัดเฉพาะการสมัครสมาชิก auto-renewable ตอนนี้รองรับ consumables, non-consumables และการสมัครสมาชิก non-renewing ตั้งแต่ iOS 16.3
คำถามที่ 3: จะกู้คืนการซื้อก่อนหน้าได้อย่างไร?
การกู้คืนการซื้อเป็นฟีเจอร์บังคับตาม 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() บังคับการซิงโครไนซ์กับเซิร์ฟเวอร์ของ Apple มีประโยชน์หลังจากการติดตั้งใหม่
SubscriptionOfferView: Merchandising การสมัครสมาชิกยุคใหม่
iOS 18.4 แนะนำ SubscriptionOfferView ซึ่งเป็นคอมโพเนนต์ SwiftUI ที่แสดงตัวเลือกการสมัครสมาชิกพร้อมการโหลด metadata ผลิตภัณฑ์อัตโนมัติ นี่เป็นหัวข้อสัมภาษณ์ทั่วไปสำหรับนักพัฒนาที่สร้างแอปที่ใช้การสมัครสมาชิก
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()
}
}พารามิเตอร์ visibleRelationship ควบคุมว่า tier การสมัครสมาชิกใดจะปรากฏ: .upgrade แสดงแพ็คเกจที่สูงกว่าหนึ่ง tier, .downgrade แสดง tier ที่ต่ำกว่า, .crossgrade แสดง tier ที่เทียบเท่ากับมูลค่าที่ดีกว่า และ .all แสดงทุกแพ็คเกจในกลุ่ม
การตรวจสอบใบเสร็จฝั่งเซิร์ฟเวอร์
การตรวจสอบฝั่งเซิร์ฟเวอร์ยังคงแนะนำสำหรับแอปพลิเคชันที่มีการสมัครสมาชิกที่สำคัญ StoreKit 2 ใช้ JWS (JSON Web Signatures) สำหรับการตรวจสอบที่ทันสมัย endpoint verifyReceipt ที่ deprecated ถูกแทนที่ด้วย 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 {
// 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?
}jwsRepresentation มีลายเซ็นเข้ารหัสที่เซิร์ฟเวอร์สามารถตรวจสอบได้ ฟิลด์ appTransactionID ใหม่ช่วยให้ติดตามลูกค้าได้อย่างแม่นยำโดยไม่ต้องมี round-trip เซิร์ฟเวอร์เพิ่มเติม
App Store Server API ของ Apple ให้การตรวจสอบฝั่งเซิร์ฟเวอร์ด้วย token JWS, Server Notifications V2 สำหรับเหตุการณ์การสมัครสมาชิกแบบเรียลไทม์ และ SubscriptionStatus API ใหม่ที่รับ Transaction IDs (iOS 18.4+) สำหรับการ query สถานะโดยตรง
การกำหนดค่าการแจ้งเตือนเซิร์ฟเวอร์
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?
}
}การแจ้งเตือนเหล่านี้ช่วยให้รักษาสถานะการสมัครสมาชิกซิงโครไนซ์กับ backend ได้
การจัดการข้อผิดพลาดและกรณีขอบ
การสัมภาษณ์มักทดสอบความเข้าใจเกี่ยวกับสถานการณ์ข้อผิดพลาดและการจัดการที่เหมาะสม สำหรับข้อมูลเพิ่มเติมเกี่ยวกับรูปแบบสถาปัตยกรรม iOS ดู คู่มือสัมภาษณ์ iOS senior เรื่อง design patterns
ข้อผิดพลาดทั่วไปใน 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
}
}การจัดการข้อผิดพลาดที่ชัดเจนช่วยปรับปรุงประสบการณ์ผู้ใช้และทำให้การดีบักง่ายขึ้น
การสนับสนุนโหมดออฟไลน์
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
}
}การแคชในเครื่องช่วยรักษาการเข้าถึงฟีเจอร์พรีเมียมแม้ไม่มีการเชื่อมต่อ
แนวทางปฏิบัติที่ดีที่สุดสำหรับการผลิต
สถาปัตยกรรมที่แนะนำ
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()
}
}สถาปัตยกรรมแบบรวมศูนย์นี้ทำให้การจัดการสถานะการซื้อทั่วทั้งแอปพลิเคชันง่ายขึ้น
แหล่งข้อมูล
- What's new in StoreKit and In-App Purchase, WWDC25 - การเพิ่ม API iOS 18.4 รวมถึง
appTransactionIDและSubscriptionOfferView - App Store Server API Documentation - การตรวจสอบฝั่งเซิร์ฟเวอร์ด้วย JWS และ Server Notifications V2
- StoreKit Documentation - อ้างอิง API ที่สมบูรณ์สำหรับ StoreKit 2
ประเด็นสำคัญสำหรับการสัมภาษณ์ StoreKit 2
StoreKit 2 เปลี่ยนแปลงการจัดการการซื้อในแอปให้เป็นประสบการณ์การพัฒนาที่ทันสมัยและปลอดภัย ประเด็นสำคัญที่ควรจดจำสำหรับการสัมภาษณ์:
- API async/await ดั้งเดิมขจัด delegate และ callback ที่ซับซ้อนจาก StoreKit 1 ซึ่งถูก deprecated ตั้งแต่ iOS 18
- การตรวจสอบอัตโนมัติผ่าน
VerificationResultจัดการการตรวจสอบเข้ารหัสของธุรกรรม Transaction.updatesให้สตรีมอะซิงโครนัสสำหรับธุรกรรมเบื้องหลังรวมถึงการต่ออายุและ family sharingTransaction.currentEntitlements(for:)แทนที่currentEntitlement(for:)ที่ deprecated สำหรับการจัดการหลาย entitlement- ฟิลด์
appTransactionID(iOS 18.4+) ช่วยให้ติดตามลูกค้าโดยไม่ต้องเรียก server-to-server SubscriptionOfferViewให้ UI ในตัวสำหรับ merchandising การสมัครสมาชิกพร้อมvisibleRelationshipสำหรับตัวเลือก upgrade, downgrade และ crossgrade- ข้อเสนอโปรโมชันสามารถเซ็นได้โดยใช้ JWS กับ
PromotionOfferV2SignatureCreator - Offer codes ตอนนี้รองรับ consumables, non-consumables และการสมัครสมาชิก non-renewing (iOS 16.3+)
- การตรวจสอบเซิร์ฟเวอร์ใช้ App Store Server API พร้อม token JWS แทนที่ endpoint
verifyReceiptที่ deprecated - เรียก
transaction.finish()เสมอหลังจากส่งมอบเนื้อหาเพื่อป้องกันพฤติกรรมการกู้คืนที่ไม่คาดคิด - ใช้การแคช entitlement ออฟไลน์เพื่อประสบการณ์ผู้ใช้ที่ราบรื่นโดยไม่ต้องเชื่อมต่อ
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
คุณหาบั๊กใน iOS เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 20 ตุลาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Swift Testing Framework สัมภาษณ์ 2026: มาโคร #expect และ #require เทียบกับ XCTest
เรียนรู้ Swift Testing Framework ใหม่สำหรับการสัมภาษณ์ iOS: มาโคร #expect และ #require การย้ายจาก XCTest แพทเทิร์นขั้นสูงและข้อผิดพลาดที่พบบ่อย

สัมภาษณ์ iOS Push Notifications 2026: APNs, โทเคน และ troubleshooting
คู่มือเตรียมสัมภาษณ์ iOS อย่างครบถ้วนเกี่ยวกับ Push Notifications, APNs, การจัดการโทเคน และ troubleshooting พร้อมคำถามยอดนิยมและคำตอบโดยละเอียด

สัมภาษณ์ iOS Senior 2026: คำถามเรื่องสถาปัตยกรรมและ Design Pattern
เตรียมตัวสัมภาษณ์ iOS senior ด้วยคำถามสำคัญเกี่ยวกับ MVVM, VIPER, Clean Architecture และ design pattern คู่มือครบถ้วนพร้อมตัวอย่างโค้ด Swift