StoreKit2 - Maintain property of purchased products

Hi everybody!

I'm desperately looking for help as I'm stuck with a rather fundamental problem regarding StoreKit2 - and maybe Swift Concurrency in general:

While renovating several freemium apps I'd like to move from local receipt validation with Receigen / OpenSSL to StoreKit2. These apps are using a dedicated "StoreManager" class which is encapsulating all App Store related operations like fetching products, performing purchases and listening on updates. For this purpose the StoreManager holds an array property with IDs of all purchased products, which is checked when a user invokes a premium function. This array can have various states during the app's life cycle:

  • Immediately after app launch (before the receipt / entitlements are checked) the array is empty
  • After checking the receipt the array holds all (locally registered) purchases
  • Later on it might change if an "Ask to Buy" purchase was approved or a purchase was performed

It is important that the array is instantly used in other (Objective-C) classes to reflect the "point in time" state of purchased products - basically acting like a cache: No async calls, completion handler, notification observer etc.

When moving to StoreKit2 the same logic applies, but the relevant API calls are (of course) in asynchronous functions: Transaction.updates triggers Transaction.currentEntitlements, which needs to update the array property. But Xcode 16 is raising a strict error because of potential data races when accessing the instance variable from an asynchronous function / actor.

What is the way to propagate IDs of purchased products app-wide without requiring every calling function as asynchronous? I'm sure I'm missing a general point with Swift Concurrency: Every example I found was working with call-backs / await, and although this talk of WWDC 2021 is addressing "protecting mutable states" I couldn't apply its outcomes to my problem. What am I missing?

Answered by MyMattes in 820558022

Problem solved!

I created a small sample project with a similar construction which just worked: Even async functions could access the class property, while my StoreManager received the following error: Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure.

By defining StoreManager class as a @MainActor, access to the mentioned property is possible across threads. All asynchronous / delaying work is done in threads, so I'm fine with forcing the StoreManager to the main thread, no performance impact can be observed.

I assume (!) my sample code was just working as its functions were located in a UIViewController, residing on the main thread anyway. But that's only an (educated) guess...

What is the way to propagate IDs of purchased products app-wide without requiring every calling function as asynchronous?

Yes and no.

The issue here is that the underlying constructs are async. If you want to access such values from a synchronous context then there’s only one viable approach: Access a snapshot of the last value you saw.

IMPORTANT A common mistake is to try to try to convert an async call into a sync call by blocking waiting for the result. This has never been a good idea and is specifically problematic with Swift concurrency. Don’t go down that path.

In your case that probably means that you want your “array property with IDs of all purchased products” to be a snapshot of what’s currently known, and then have a mechanism whereby the clients of that array can be notified when it changes.

How you do that depends on the details of those clients. It sounds like you have a mix of Swift and Objective-C, and some of those clients are in Objective-C. Is that right?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi Quinn,

It sounds like you have a mix of Swift and Objective-C, and some of those clients are in Objective-C. Is that right?

Exactly! Actually, most of those clients are currently Objective-C classes because StoreKit2 as Swift-only is the main reason to migrate StoreManager.

I totally agree to your mention of a "snapshot" - that's what I meant when calling the array a kind of cache, for the lack of better words: It only provides the "point-in-time" state of purchased products, and all clients are notified via NSNotificationCenter when their data was updated. Access to the array property is always in sync calls without blocking / waiting for any asynchronous call to finish.

Accepted Answer

Problem solved!

I created a small sample project with a similar construction which just worked: Even async functions could access the class property, while my StoreManager received the following error: Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure.

By defining StoreManager class as a @MainActor, access to the mentioned property is possible across threads. All asynchronous / delaying work is done in threads, so I'm fine with forcing the StoreManager to the main thread, no performance impact can be observed.

I assume (!) my sample code was just working as its functions were located in a UIViewController, residing on the main thread anyway. But that's only an (educated) guess...

StoreKit2 - Maintain property of purchased products
 
 
Q