I encountered an error during the deinit process of a class, and I suspect the issue stems from using the @Observable macro. Specifically, I’m getting the following error message:
// TODO: ERROR: Main actor-isolated property 'updateListenerTask' cannot be referenced from a nonisolated context It seems like the property updateListenerTask is marked as @MainActor, but the deinit method isn't being executed in a MainActor-isolated context. This leads to the error when trying to clean up or interact with the task.
I’m curious if anyone has tips or best practices for handling these kinds of issues. Adding @Mainactor to the function does not work.
import Foundation
import StoreKit
import SwiftUI
public typealias Transaction = StoreKit.Transaction
public typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
public typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState
@MainActor
@Observable
public class Store {
// All available Products
private(set) var consumables: [Product]
private(set) var subscriptions: [Product]
// Check renewal state for subscription. Not yet implemented
private(set) var subscriptionGroupStatus: RenewalState?
// This is the purchased product
private(set) var purchasedSubscriptionLevel: SubscriptionLevel?
// This can be multiple subscriptions. For now the implementation is that there is one subscription group!
private var purchasedSubscriptions: [Product] = []
private var productIDs = ProductIdentifiers.shared.getAllProductIDs()
private var updateListenerTask: Task<Void, Error>?
private var isPurchaseInProgress: Bool = false
public init() {
// Initialize empty products, and then do a product request asynchronously to fill them in.
consumables = []
subscriptions = []
// Start a transaction listener as close to app launch as possible so you don't miss any transactions.
updateListenerTask = listenForTransactions()
Task {
// During store initialization, request products from the App Store.
await requestProducts()
// Deliver products that the customer purchases.
await updateCustomerProductStatus()
}
}
// TODO: ERROR: Main actor-isolated property 'updateListenerTask' can not be referenced from a nonisolated context
deinit {
updateListenerTask?.cancel()
}
private func listenForTransactions() -> Task<Void, Error> {
Task.detached {
// Iterate through any transactions that don't come from a direct call to `purchase()`.
for await result in Transaction.updates {
do {
let transaction = try await self.checkVerified(result)
// Deliver products to the user.
await self.updateCustomerProductStatus()
// Always finish a transaction.
await transaction.finish()
} catch {
// StoreKit has a transaction that fails verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
private func requestProducts() async {
do {
// Request products from the App Store using a ProductIdentifiers in StoreKitConfiguration
let storeProducts = try await Product.products(for: productIDs)
var newSubscriptions: [Product] = []
var newConsumable: [Product] = []
// Filter the products into categories based on their type.
for product in storeProducts {
switch product.type {
case .consumable:
newConsumable.append(product)
case .nonConsumable:
// Ignore this product.
print("No non consumables at the moment")
case .autoRenewable:
newSubscriptions.append(product)
case .nonRenewable:
print("No non renawbles at the moment")
default:
// Ignore this product.
print("Unknown product")
}
}
// Add to Observable variable
subscriptions = newSubscriptions
consumables = newConsumable
} catch {
print("Failed product request from the App Store server: \(error)")
}
}
private func purchase(_ product: Product) async throws -> Transaction? {
// Begin purchasing the `Product` the user selects.
let result = try await product.purchase()
switch result {
case let .success(verification):
// Check whether the transaction is verified. If it isn't,
// this function rethrows the verification error.
let transaction = try checkVerified(verification)
// The transaction is verified. Deliver content to the user.
await updateCustomerProductStatus()
// Always finish a transaction.
await transaction.finish()
return transaction
case .userCancelled, .pending:
return nil
default:
return nil
}
}
private func isPurchased(_ product: Product) async throws -> Bool {
// Determine whether the user purchases a given product.
switch product.type {
case .nonRenewable:
return false
case .nonConsumable:
return false
case .autoRenewable:
return purchasedSubscriptions.contains(product)
default:
return false
}
}
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
// Check whether the JWS passes StoreKit verification.
switch result {
case .unverified:
// StoreKit parses the JWS, but it fails verification.
throw StoreError.failedVerification
case let .verified(safe):
// The result is verified. Return the unwrapped value.
return safe
}
}
private func updateCustomerProductStatus() async {
var purchasedSubscriptions: [Product] = []
// Iterate through all of the user's purchased products.
for await result in Transaction.currentEntitlements {
do {
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
let transaction = try checkVerified(result)
// Check the `productType` of the transaction and get the corresponding product from the store.
switch transaction.productType {
case .autoRenewable:
if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) {
purchasedSubscriptions.append(subscription)
purchasedSubscriptionLevel = SubscriptionLevel(subscription)
}
default:
break
}
} catch {
print("Failed to verify transaction: \(error.localizedDescription)")
}
}
// Update the store information with auto-renewable subscription products.
self.purchasedSubscriptions = purchasedSubscriptions
// Check the `subscriptionGroupStatus` to learn the auto-renewable subscription state to determine whether the customer
// is new (never subscribed), active, or inactive (expired subscription). This app has only one subscription
// group, so products in the subscriptions array all belong to the same group. The statuses that
// `product.subscription.status` returns apply to the entire subscription group.
subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.first?.state
}
private func startPurchase() -> Bool {
if isPurchaseInProgress {
return false
}
isPurchaseInProgress = true
return true
}
private func endPurchase() {
isPurchaseInProgress = false
}
private func performPurchase(for product: Product) async throws -> Transaction {
guard startPurchase() else {
throw PurchaseError.purchaseInProgress
}
defer {
endPurchase()
}
do {
if let transaction = try await purchase(product) {
return transaction
} else {
throw PurchaseError.failedPurchase // Or create a new error like PurchaseError.userCancelled
}
} catch {
throw error
}
}
}
// MARK: Helper functions
public extension Store {
func isSubscribed() -> Bool {
return purchasedSubscriptionLevel != nil
}
}
// MARK: Purchase functions
public extension Store {
func purchaseSubscription(for product: Product) async throws -> Transaction {
guard purchasedSubscriptions.isEmpty else {
throw PurchaseError.alreadySubscribed
}
do {
let transaction = try await performPurchase(for: product)
print("LOGC transactionid: \(transaction)")
return transaction
} catch StoreError.failedVerification {
throw PurchaseError.failedVerification
} catch {
throw PurchaseError.failedPurchase
}
}
}
public enum StoreError: Error {
case failedVerification
}
enum PurchaseError: Error {
case purchaseInProgress
case productNotFound
case failedPurchase
case failedVerification
case alreadySubscribed
var message: String {
switch self {
case .purchaseInProgress:
return "Another purchase is in progress."
case .productNotFound:
return "Product not found."
case .failedPurchase:
return "Failed to complete the purchase."
case .failedVerification:
return "Could not verify purchase with the Apple Store."
case .alreadySubscribed:
return "You are already subscribed."
}
}
}
I didn't know what @Observable
do under the hood (as I think it might convert your code to computed property), but if I remove @Observable
your code compile successfully. In your code updateListenerTask
is a private property so there is no need to notify when its value change, so a way to acheive your goal is mark updateListenerTask
with macro ObservationIgnored
:
@ObservationIgnored private var updateListenerTask: Task<Void, Error>?
Also there will be Isolated synchronous deinit
implemented in this SE-0371 which will available in Swift 6.1