Skip to main content

Rewarded Ads

Rewarded ads let users opt-in to watch a video ad in exchange for an in-app reward (extra lives, premium content, virtual currency, etc.).

Each ad format uses a Zone ID to identify the ad placement. Zone IDs are configured in the Empower dashboard.

Note: All ad status listeners are optional. The SDK handles ad loading and display automatically. Use listeners only if you need to track ad states for analytics, UI updates, or custom logic.


Quick Start

import EmpowerMobileAds

class StoreViewController: UIViewController, AdStatusDelegate {

private let rewardedZoneId = "YOUR_REWARDED_ZONE_ID"

override func viewDidLoad() {
super.viewDidLoad()
loadRewardedAd()
}

func loadRewardedAd() {
EMAManager.shared.loadRewardedAd(zoneId: rewardedZoneId, delegate: self)
}

@IBAction func watchAdButtonTapped(_ sender: UIButton) {
if EMAManager.shared.isRewardedReady(zoneId: rewardedZoneId) {
EMAManager.shared.showRewarded(zoneId: rewardedZoneId, from: self)
} else {
showAlert("Ad not ready", "Please try again in a moment.")
}
}

func rewardedStatusChanged(adStatus: AdStatus) {
switch adStatus {
case .ready:
enableWatchAdButton()
case .rewarded(let type, let amount):
// USER COMPLETED THE AD — grant the reward
grantReward(amount)
case .closed, .skipped:
loadRewardedAd() // Preload next
case .failed:
disableWatchAdButton()
default:
break
}
}

private func grantReward(_ amount: Int) {
UserManager.shared.addCoins(amount)
showAlert("Reward Earned!", "You received \(amount) coins!")
}
}

Important: Reward Callback

Rewarded status updates are delivered through the single status callback (rewardedStatusChanged in Swift, rewardedStatusChanged:rewardType:rewardAmount: in Objective-C).

Grant the reward only when the status is rewarded. This status fires exactly once, when the user successfully finishes watching the ad. On dismissal you get one of two statuses — never grant on either: .closed when the user earned the reward (it arrives right after .rewarded), or .skipped when the user closed the ad early without earning it (no preceding .rewarded).

func rewardedStatusChanged(adStatus: AdStatus) {
switch adStatus {
case .rewarded(let type, let amount):
// CORRECT — user earned the reward
grantReward(amount)
case .closed, .skipped:
// Ad dismissed — .closed = reward earned, .skipped = closed without a reward.
// Do NOT grant here; only grant on .rewarded.
loadRewardedAd()
default:
break
}
}

The rewardType / rewardAmount parameters (Objective-C) and the .rewarded(type:amount:) associated values (Swift) carry the reward payload configured for the zone.


Loading Rewarded Ads

EMAManager.shared.loadRewardedAd(zoneId: "YOUR_ZONE_ID", delegate: self)

Keeping an Ad Ready

The SDK preloads eligible rewarded zones automatically based on your server configuration, so an ad is typically ready shortly after loadRewardedAd. Showing a rewarded ad consumes it, so to always have the next one ready, call loadRewardedAd again from the .closed / .skipped status (as the examples below do).


Showing Rewarded Ads

Always verify the ad is ready before showing:

func showRewardedAd() {
let zoneId = "YOUR_ZONE_ID"

if EMAManager.shared.isRewardedReady(zoneId: zoneId) {
EMAManager.shared.showRewarded(zoneId: zoneId, from: self)
} else {
showAlert("Not Ready", "Ad is still loading. Please try again.")
}
}

Status Handling

extension StoreViewController: AdStatusDelegate {

func rewardedStatusChanged(adStatus: AdStatus) {
switch adStatus {
case .initializing:
watchAdButton.isEnabled = false
watchAdButton.setTitle("Loading...", for: .normal)

case .ready:
watchAdButton.isEnabled = true
watchAdButton.setTitle("Watch Ad for 100 Coins", for: .normal)

case .shown:
pauseBackgroundMusic()

case .rewarded(_, let amount):
// User completed the ad — grant the reward
grantReward(amount)

case .closed, .skipped:
// .closed = reward earned, .skipped = dismissed without a reward.
resumeBackgroundMusic()
loadRewardedAd() // Preload next

case .failed:
watchAdButton.isEnabled = false
watchAdButton.setTitle("Ad Unavailable", for: .normal)
retryLoadAfterDelay()

default:
break
}
}

private func retryLoadAfterDelay() {
DispatchQueue.main.asyncAfter(deadline: .now() + 30) { [weak self] in
self?.loadRewardedAd()
}
}
}

Status Flow

loadRewardedAd() → .initializing → .ready → showRewarded() → .shown → user completes → .rewarded → .closed
↓ ↓
.failed user skips → .skipped (no reward)

Status Values

Swift (AdStatus)Objective-C (EMAAdStatusType)Description
.initializingEMAAdStatusTypeInitializingAd is loading
.readyEMAAdStatusTypeReadyAd is ready to show
.shownEMAAdStatusTypeShownAd is currently showing
.rewarded(type:amount:)EMAAdStatusTypeRewardedUser completed the ad and earned the reward
.failed(Error?)EMAAdStatusTypeFailedAd failed to load
.closedEMAAdStatusTypeClosedDismissed after earning the reward (follows .rewarded)
.skippedEMAAdStatusTypeSkippedDismissed without earning the reward

Releasing a Zone

EMAManager.shared.destroyRewardedZone(zoneId: "YOUR_ZONE_ID")

SwiftUI Integration

import SwiftUI
import EmpowerMobileAds

// MARK: - Rewarded Ad Coordinator

class RewardedAdCoordinator: NSObject, ObservableObject, AdStatusDelegate {
@Published var isReady = false
@Published var isLoading = true

private let zoneId: String
var onRewardEarned: ((Int) -> Void)?

init(zoneId: String) {
self.zoneId = zoneId
super.init()
load()
}

func load() {
isLoading = true
EMAManager.shared.loadRewardedAd(zoneId: zoneId, delegate: self)
}

func show() {
guard isReady,
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else { return }

EMAManager.shared.showRewarded(zoneId: zoneId, from: rootVC)
}

func rewardedStatusChanged(adStatus: AdStatus) {
DispatchQueue.main.async {
switch adStatus {
case .ready:
self.isReady = true
self.isLoading = false
case .failed:
self.isReady = false
self.isLoading = false
case .rewarded(_, let amount):
self.onRewardEarned?(amount)
case .closed, .skipped:
self.isReady = false
self.load() // Reload
default:
break
}
}
}
}

// MARK: - Usage in SwiftUI View

struct StoreView: View {
@StateObject private var rewardedAd = RewardedAdCoordinator(zoneId: "YOUR_ZONE_ID")
@State private var coins = 0
@State private var showRewardAlert = false

var body: some View {
VStack(spacing: 20) {
Text("Coins: \(coins)")
.font(.largeTitle)

Button(action: { rewardedAd.show() }) {
Text(rewardedAd.isReady ? "Watch Ad for 100 Coins" : "Ad Unavailable")
.frame(maxWidth: .infinity)
.padding()
.background(rewardedAd.isReady ? Color.blue : Color.gray)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(!rewardedAd.isReady)
}
.padding()
.onAppear {
rewardedAd.onRewardEarned = { amount in
coins += amount
showRewardAlert = true
}
}
.alert("Reward Earned!", isPresented: $showRewardAlert) {
Button("OK", role: .cancel) {}
} message: {
Text("You received your reward!")
}
}
}

Best Practices

  1. Grant the reward only on .rewarded — never on .closed or .skipped. This is the most common rewarded integration mistake. .rewarded fires once, only when the user actually earns the reward.
  2. Handle both dismiss statuses — run your cleanup and reload on both .closed (dismissed after earning) and .skipped (dismissed without earning), so a skipped ad still resets your UI and reloads the next ad.
  3. Gate the entry point on readiness — enable your "watch ad" button on .ready and disable it on .failed; check isRewardedReady(zoneId:) before calling showRewarded.
  4. Don't stack full-screen ads — don't present a rewarded ad while another full-screen ad is up; guard with isAnyFullScreenAdShowing().
  5. Reload after dismissal — reload from .closed / .skipped. (The SDK also auto-reloads when the zone's server-side autoReload is on, so reloading yourself is safe either way.)

Troubleshooting

Reward Not Granted

  1. Grant the reward in the rewarded status only (not on closed).
  2. Check the delegate/observer is set when loading.
  3. Verify the ad completed (a skipped ad never reports rewarded).

Ad Not Loading

print("Is ready: \(EMAManager.shared.isRewardedReady(zoneId: "YOUR_ZONE_ID"))")
EMASettings.shared.logLevel = .all

API Reference

func loadRewardedAd(zoneId: String, delegate: AdStatusDelegate? = nil)
func showRewarded(zoneId: String, from viewController: UIViewController)
func isRewardedReady(zoneId: String) -> Bool
func isRewardedShowing(zoneId: String) -> Bool
func destroyRewardedZone(zoneId: String)

Objective-C selectors

SwiftObjective-C selector
loadRewardedAd(zoneId:delegate:)loadRewardedAdWithZoneId:observer:
showRewarded(zoneId:from:)showRewardedWithZoneId:fromViewController:
isRewardedReady(zoneId:)isRewardedReadyForZoneId:
isRewardedShowing(zoneId:)isRewardedShowingForZoneId:
destroyRewardedZone(zoneId:)destroyRewardedZone:

In Objective-C, adopt EMAAdObserver and implement rewardedStatusChanged:rewardType:rewardAmount:. Grant the reward when status is EMAAdStatusTypeRewarded.