Skip to content

[iOS] Clarify thread-safety for MARewardedAd.isReady / MAInterstitialAd.isReady (possible data race) #467

@maximkir-fl

Description

@maximkir-fl

isReady is frequently consulted from non-main actors/threads (for example, inside Swift-Concurrency code).
Because the AppLovin SDK updates its internal state on the main thread (delegate callbacks),
reading isReady off the main thread creates a data race that can cause false negatives.
In production, we observe time-out warnings like

Timeout expired, ad not ready for adUnitId: XXXXXX

even though the ad finished loading seconds earlier.


Repro project

  1. Create a small Swift app:

    let ad = MARewardedAd.shared(withAdUnitIdentifier: "«TEST_AD_UNIT»")
    ad.delegate = self
    ad.loadAd()
    
    Task.detached {               // background executor
        while true {
            print("isReady:", ad.isReady)
            try? await Task.sleep(nanoseconds: 50_000_000)   // 50 ms
        }
    }
  2. Observe console output right after didLoad fires on the main thread:
    isReady prints false for several iterations before flipping to true
    (sometimes it never flips).


Expected behaviour

• Either
– Accessing isReady from any thread/actor is documented as unsafe,
or
– The property is made thread-safe / @MainActor-isolated so that mixed-thread
access is benign.


Actual behaviour

Reading from non-main threads sometimes returns stale false, which makes
apps implement unnecessary retry/timeout logic and degrades UX.


Environment

• AppLovin MAX SDK version: 13.0.0
• Xcode version: 16.4
• iOS deployment target: 15
• Device / OS: occurs on both Simulator and Device (iOS 17–18)


Proposed solutions

  1. Document clearly (in headers & guides) that all MAX API calls must occur on the main thread / @MainActor.

  2. Objective-C header enforcement

    #define MA_REQUIRES_MAIN_THREAD  __attribute__((objc_requires_main_queue))
    
    @property (nonatomic, assign, readonly, getter=isReady) BOOL ready MA_REQUIRES_MAIN_THREAD;
  3. Swift overlay

    @MainActor public var isReady: Bool { get }
  4. Swift-6–readiness (delegate protocols)
    Swift 6 ships with Strict Concurrency enabled; compiling the current SDK
    with -warn-concurrency emits hundreds of warnings.
    Please mark every delegate protocol as @MainActor and, where applicable,
    wrap Obj-C protocols with a Swift overlay:

    /// Always invoked on the main thread.
    @protocol MARewardedAdDelegate <MAAdDelegate>
    - (void)didLoadAd:(MAAd *)ad;
    …
    @end
    @MainActor
    public protocol MARewardedAdDelegate: MAAdDelegate {
        func didLoad(_ ad: MAAd)}

    The same applies to
    MAInterstitialAdDelegate
    MABannerAdDelegate
    • any other MAX delegate that is guaranteed to run on the main queue.

These changes will eliminate the current data race, silence Swift-6
concurrency warnings, and give developers compile-time guarantees about thread
safety.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions