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
- Swift
- Objective-C
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!")
}
}
@import EmpowerMobileAds;
@interface StoreViewController () <EMAAdObserver>
@end
@implementation StoreViewController
static NSString *const rewardedZoneId = @"YOUR_REWARDED_ZONE_ID";
- (void)viewDidLoad {
[super viewDidLoad];
[self loadRewardedAd];
}
- (void)loadRewardedAd {
[[EMAManager shared] loadRewardedAdWithZoneId:rewardedZoneId observer:self];
}
- (IBAction)watchAdButtonTapped:(id)sender {
if ([[EMAManager shared] isRewardedReadyForZoneId:rewardedZoneId]) {
[[EMAManager shared] showRewardedWithZoneId:rewardedZoneId fromViewController:self];
} else {
[self showAlert:@"Ad not ready" message:@"Please try again in a moment."];
}
}
- (void)rewardedStatusChanged:(EMAAdStatusType)status
rewardType:(NSString *)rewardType
rewardAmount:(NSInteger)rewardAmount {
switch (status) {
case EMAAdStatusTypeReady:
[self enableWatchAdButton];
break;
case EMAAdStatusTypeRewarded:
// USER COMPLETED THE AD — grant the reward
[self grantReward:rewardAmount];
break;
case EMAAdStatusTypeClosed:
case EMAAdStatusTypeSkipped:
[self loadRewardedAd]; // Preload next
break;
case EMAAdStatusTypeFailed:
[self disableWatchAdButton];
break;
default:
break;
}
}
- (void)grantReward:(NSInteger)amount {
[UserManager.shared addCoins:amount];
[self showAlert:@"Reward Earned!"
message:[NSString stringWithFormat:@"You received %ld coins!", (long)amount]];
}
@end
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).
- Swift
- Objective-C
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
}
}
- (void)rewardedStatusChanged:(EMAAdStatusType)status
rewardType:(NSString *)rewardType
rewardAmount:(NSInteger)rewardAmount {
if (status == EMAAdStatusTypeRewarded) {
// CORRECT — user earned the reward
[self grantReward:rewardAmount];
} else if (status == EMAAdStatusTypeClosed || status == EMAAdStatusTypeSkipped) {
// Ad dismissed (Closed = earned, Skipped = closed without a reward). Do NOT grant here.
[self loadRewardedAd];
}
}
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
- Swift
- Objective-C
EMAManager.shared.loadRewardedAd(zoneId: "YOUR_ZONE_ID", delegate: self)
[[EMAManager shared] loadRewardedAdWithZoneId:@"YOUR_ZONE_ID" observer: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:
- Swift
- Objective-C
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.")
}
}
- (void)showRewardedAd {
NSString *zoneId = @"YOUR_ZONE_ID";
if ([[EMAManager shared] isRewardedReadyForZoneId:zoneId]) {
[[EMAManager shared] showRewardedWithZoneId:zoneId fromViewController:self];
} else {
[self showAlert:@"Not Ready" message:@"Ad is still loading. Please try again."];
}
}
Status Handling
- Swift
- Objective-C
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()
}
}
}
- (void)rewardedStatusChanged:(EMAAdStatusType)status
rewardType:(NSString *)rewardType
rewardAmount:(NSInteger)rewardAmount {
switch (status) {
case EMAAdStatusTypeInitializing:
self.watchAdButton.enabled = NO;
[self.watchAdButton setTitle:@"Loading..." forState:UIControlStateNormal];
break;
case EMAAdStatusTypeReady:
self.watchAdButton.enabled = YES;
[self.watchAdButton setTitle:@"Watch Ad for 100 Coins" forState:UIControlStateNormal];
break;
case EMAAdStatusTypeShown:
[self pauseBackgroundMusic];
break;
case EMAAdStatusTypeRewarded:
// User completed the ad — grant the reward
[self grantReward:rewardAmount];
break;
case EMAAdStatusTypeClosed: // reward earned
case EMAAdStatusTypeSkipped: // dismissed without a reward
[self resumeBackgroundMusic];
[self loadRewardedAd]; // Preload next
break;
case EMAAdStatusTypeFailed:
self.watchAdButton.enabled = NO;
[self.watchAdButton setTitle:@"Ad Unavailable" forState:UIControlStateNormal];
[self retryLoadAfterDelay];
break;
default:
break;
}
}
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 |
|---|---|---|
.initializing | EMAAdStatusTypeInitializing | Ad is loading |
.ready | EMAAdStatusTypeReady | Ad is ready to show |
.shown | EMAAdStatusTypeShown | Ad is currently showing |
.rewarded(type:amount:) | EMAAdStatusTypeRewarded | User completed the ad and earned the reward |
.failed(Error?) | EMAAdStatusTypeFailed | Ad failed to load |
.closed | EMAAdStatusTypeClosed | Dismissed after earning the reward (follows .rewarded) |
.skipped | EMAAdStatusTypeSkipped | Dismissed without earning the reward |
Releasing a Zone
- Swift
- Objective-C
EMAManager.shared.destroyRewardedZone(zoneId: "YOUR_ZONE_ID")
[[EMAManager shared] destroyRewardedZone:@"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
- Grant the reward only on
.rewarded— never on.closedor.skipped. This is the most common rewarded integration mistake..rewardedfires once, only when the user actually earns the reward. - 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. - Gate the entry point on readiness — enable your "watch ad" button on
.readyand disable it on.failed; checkisRewardedReady(zoneId:)before callingshowRewarded. - Don't stack full-screen ads — don't present a rewarded ad while another full-screen ad is up;
guard with
isAnyFullScreenAdShowing(). - Reload after dismissal — reload from
.closed/.skipped. (The SDK also auto-reloads when the zone's server-sideautoReloadis on, so reloading yourself is safe either way.)
Troubleshooting
Reward Not Granted
- Grant the reward in the
rewardedstatus only (not onclosed). - Check the delegate/observer is set when loading.
- Verify the ad completed (a skipped ad never reports
rewarded).
Ad Not Loading
- Swift
- Objective-C
print("Is ready: \(EMAManager.shared.isRewardedReady(zoneId: "YOUR_ZONE_ID"))")
EMASettings.shared.logLevel = .all
NSLog(@"Is ready: %d", [[EMAManager shared] isRewardedReadyForZoneId:@"YOUR_ZONE_ID"]);
EMASettings.shared.logLevel = LogLevelAll;
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
| Swift | Objective-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
EMAAdObserverand implementrewardedStatusChanged:rewardType:rewardAmount:. Grant the reward whenstatusisEMAAdStatusTypeRewarded.