Banner Ads
Banner ads are rectangular ads that occupy a portion of your app's layout. They can refresh automatically and stay on screen while users interact with your app.
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 ViewController: UIViewController, AdStatusDelegate {
@IBOutlet weak var bannerContainer: UIView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadBanner()
}
func loadBanner() {
EMAManager.shared.loadBannerAd(
viewController: self,
zoneId: "YOUR_BANNER_ZONE_ID",
bannerContainer: bannerContainer,
delegate: self
)
}
func bannerStatusChanged(status: AdStatus, zoneId: String, adUnitId: String) {
switch status {
case .ready:
print("Banner is ready and displayed for zone: \(zoneId)")
case .failed(let error):
print("Banner failed to load: \(error?.localizedDescription ?? "unknown")")
case .initializing:
print("Banner is loading...")
default:
break
}
}
}
@import EmpowerMobileAds;
@interface ViewController () <EMAAdObserver>
@property (nonatomic, weak) IBOutlet UIView *bannerContainer;
@end
@implementation ViewController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self loadBanner];
}
- (void)loadBanner {
[[EMAManager shared] loadBannerAdWithViewController:self
zoneId:@"YOUR_BANNER_ZONE_ID"
container:self.bannerContainer
observer:self
keywords:nil];
}
- (void)bannerStatusChanged:(EMAAdStatusType)status
zoneId:(NSString *)zoneId
adUnitId:(NSString *)adUnitId {
switch (status) {
case EMAAdStatusTypeReady:
NSLog(@"Banner is ready and displayed for zone: %@", zoneId);
break;
case EMAAdStatusTypeFailed:
NSLog(@"Banner failed to load");
break;
case EMAAdStatusTypeInitializing:
NSLog(@"Banner is loading...");
break;
default:
break;
}
}
@end
Supported Sizes
| Size | Description |
|---|---|
| 320x50 | Standard Banner |
| 320x100 | Large Banner |
| 300x250 | Medium Rectangle |
Adaptive & Custom Sizes
By default a banner renders at the backend-configured size (.fixed). You can override the size for
a single load with the sizeMode parameter — the ad unit IDs still come from the backend, so
this changes only how the ad is sized and rendered, not which units are requested. Anchored
adaptive sizes generally earn more than a fixed 320×50.
EMABannerSizeMode | Behavior |
|---|---|
.fixed (default) | Backend-provided adSize — unchanged behavior. |
.adaptiveAnchored | Anchored adaptive: Google standard anchored + AppLovin adaptive, both resolved to the same height for the container width (tight fit). |
.adaptiveAnchoredLarge | Google's large anchored variant (taller → higher revenue); AppLovin serves its standard adaptive height, centered in the taller container. |
.custom(width:height:) | Caller-provided size. width == 0 means "use the container's width". Google / own-view honor the exact size; AppLovin renders its nearest fixed format centered. |
- Swift
- Objective-C
EMAManager.shared.loadBannerAd(
viewController: self,
zoneId: "YOUR_BANNER_ZONE_ID",
bannerContainer: bannerContainer,
delegate: self,
sizeMode: .adaptiveAnchored
)
Objective-C can't use Swift's associated-value enum, so pass an Int-backed adaptiveMode plus an
explicit width/height (used only for .custom; a customWidth of 0 means "container width"):
[[EMAManager shared] loadBannerAdWithViewController:self
zoneId:@"YOUR_BANNER_ZONE_ID"
container:bannerContainer
observer:self
keywords:nil
adaptiveMode:EMABannerAdaptiveModeAdaptiveAnchored
customWidth:0
customHeight:0];
Notes: size the container for the adaptive height (anchored adaptive is often taller than 50pt), and for anchored modes pin the container's width to the screen. MREC (300×250) zones ignore anchored modes (they stay 300×250);
.customis always honored. Adaptive/custom controls the request size — fill still needs matching creatives trafficked on the ad unit, so an unusual custom size may simply no-fill.
Container Setup
Interface Builder (Storyboard)
- Add a
UIViewto your view controller - Set constraints for position and height
- Connect the outlet to your view controller
Programmatic Setup
// Standard Banner (320x50)
let bannerContainer = UIView()
bannerContainer.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerContainer)
NSLayoutConstraint.activate([
bannerContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bannerContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bannerContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bannerContainer.heightAnchor.constraint(equalToConstant: 50)
])
// MREC (300x250)
let mrecContainer = UIView()
mrecContainer.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mrecContainer)
NSLayoutConstraint.activate([
mrecContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
mrecContainer.topAnchor.constraint(equalTo: someView.bottomAnchor, constant: 16),
mrecContainer.widthAnchor.constraint(equalToConstant: 300),
mrecContainer.heightAnchor.constraint(equalToConstant: 250)
])
Loading Options
Basic Loading
- Swift
- Objective-C
EMAManager.shared.loadBannerAd(
viewController: self,
zoneId: "YOUR_ZONE_ID",
bannerContainer: bannerContainer,
delegate: self
)
[[EMAManager shared] loadBannerAdWithViewController:self
zoneId:@"YOUR_ZONE_ID"
container:self.bannerContainer
observer:self
keywords:nil];
With Keywords
- Swift
- Objective-C
EMAManager.shared.loadBannerAd(
viewController: self,
zoneId: "YOUR_ZONE_ID",
bannerContainer: bannerContainer,
delegate: self,
keywords: "sports,football,news"
)
[[EMAManager shared] loadBannerAdWithViewController:self
zoneId:@"YOUR_ZONE_ID"
container:self.bannerContainer
observer:self
keywords:@"sports,football,news"];
Legacy Loading (without ViewController)
Note: It's recommended to use the method that accepts a
viewControllerparameter for proper lifecycle management. In Objective-C, useloadBannerAdWithViewController:zoneId:container:observer:keywords:.
EMAManager.shared.loadBannerAd(
zoneId: "YOUR_ZONE_ID",
container: bannerContainer,
delegate: self,
customParameters: "sports,football"
)
In a List (UITableView / UICollectionView)
For banners inside recycling cells, use the cell-aware load method — pass the cell so the SDK
ties the ad to the cell's reuse. A recycled cell then never shows a stale ad from another row, and
the zone is paused/resumed as the row scrolls in and out, instead of being torn down and reloaded on
every recycle. Don't use the plain loadBannerAd for cells.
Add the container to cell.contentView, then load with the cell (from cellForRowAt):
- Swift
- Objective-C
EMAManager.shared.loadBannerAd(
cell: cell,
zoneId: "YOUR_ZONE_ID",
bannerContainer: container, // already added to cell.contentView
delegate: self
)
[[EMAManager shared] loadBannerAdInCell:cell
zoneId:@"YOUR_ZONE_ID"
container:container
observer:self
keywords:nil];
That's all. When you load with the cell: API the SDK observes the cell directly and tracks its
on-screen visibility and recycling automatically — it pauses the ad when the row scrolls
off-screen, resumes it when it returns, and clears it on cell reuse. You do not need to hook
willDisplay / didEndDisplaying or call notifyBannerVisible / notifyBannerInvisible.
In a Scroll View (or any other scrolling container)
For a banner inside a plain UIScrollView, a SwiftUI ScrollView, a page / carousel view, or any
container that is not a recycling UITableView / UICollectionView cell, just place the banner
container in your layout and load it the normal way with
loadBannerAd(viewController:zoneId:bannerContainer:delegate:). The SDK tracks the container's
actual on-screen position and automatically pauses the ad when it scrolls off-screen and resumes
it when it comes back — there is nothing extra to call. Visibility is geometry-based: the ad
counts as visible while roughly ≥10% of the container is on screen.
Only recycling cells need the cell: API above (for reuse correctness); every other scrolling
layout is handled by this automatic tracking.
Status Handling
Status Callback
- Swift
- Objective-C
Adopt AdStatusDelegate and implement bannerStatusChanged(status:zoneId:adUnitId:):
extension ViewController: AdStatusDelegate {
func bannerStatusChanged(status: AdStatus, zoneId: String, adUnitId: String) {
switch status {
case .initializing:
// Banner is loading
showLoadingIndicator()
case .ready:
// Banner is displayed
hideLoadingIndicator()
print("Banner loaded for zone: \(zoneId), adUnit: \(adUnitId)")
case .failed(let error):
// Banner failed to load
hideLoadingIndicator()
hideBannerContainer()
print("Banner failed for zone: \(zoneId), error: \(error?.localizedDescription ?? "unknown")")
case .clicked:
print("Banner clicked for zone: \(zoneId)")
case .impression:
print("Banner impression recorded for zone: \(zoneId)")
default:
break
}
}
}
Adopt EMAAdObserver and implement bannerStatusChanged:zoneId:adUnitId::
- (void)bannerStatusChanged:(EMAAdStatusType)status
zoneId:(NSString *)zoneId
adUnitId:(NSString *)adUnitId {
switch (status) {
case EMAAdStatusTypeInitializing:
[self showLoadingIndicator];
break;
case EMAAdStatusTypeReady:
[self hideLoadingIndicator];
NSLog(@"Banner loaded for zone: %@, adUnit: %@", zoneId, adUnitId);
break;
case EMAAdStatusTypeFailed:
[self hideLoadingIndicator];
[self hideBannerContainer];
NSLog(@"Banner failed for zone: %@", zoneId);
break;
case EMAAdStatusTypeClicked:
NSLog(@"Banner clicked for zone: %@", zoneId);
break;
case EMAAdStatusTypeImpression:
NSLog(@"Banner impression recorded for zone: %@", zoneId);
break;
default:
break;
}
}
Status Values
Swift (AdStatus) | Objective-C (EMAAdStatusType) | Description |
|---|---|---|
.initializing | EMAAdStatusTypeInitializing | Banner is loading |
.ready | EMAAdStatusTypeReady | Banner is displayed |
.failed(Error?) | EMAAdStatusTypeFailed | Banner failed to load |
.clicked | EMAAdStatusTypeClicked | Banner was clicked |
.impression | EMAAdStatusTypeImpression | Impression was recorded |
.undefined | EMAAdStatusTypeUndefined | Initial/destroyed state |
SwiftUI Integration
import SwiftUI
import EmpowerMobileAds
// MARK: - Banner View
struct BannerAdView: UIViewControllerRepresentable {
let zoneId: String
let height: CGFloat
func makeUIViewController(context: Context) -> BannerAdViewController {
return BannerAdViewController(zoneId: zoneId)
}
func updateUIViewController(_ uiViewController: BannerAdViewController, context: Context) {}
}
// MARK: - Banner View Controller
class BannerAdViewController: UIViewController, AdStatusDelegate {
private let zoneId: String
private var bannerContainer: UIView!
init(zoneId: String) {
self.zoneId = zoneId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupContainer()
loadBanner()
}
private func setupContainer() {
bannerContainer = UIView()
bannerContainer.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerContainer)
NSLayoutConstraint.activate([
bannerContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bannerContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bannerContainer.topAnchor.constraint(equalTo: view.topAnchor),
bannerContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func loadBanner() {
EMAManager.shared.loadBannerAd(
viewController: self,
zoneId: zoneId,
bannerContainer: bannerContainer,
delegate: self
)
}
func bannerStatusChanged(status: AdStatus, zoneId: String, adUnitId: String) {
// Handle status changes
}
}
// MARK: - Usage
struct ContentView: View {
var body: some View {
VStack {
Text("Your Content")
Spacer()
// Standard Banner
BannerAdView(zoneId: "YOUR_BANNER_ZONE_ID", height: 50)
.frame(height: 50)
}
}
}
struct ArticleView: View {
var body: some View {
ScrollView {
VStack {
Text("Article Title")
.font(.title)
// MREC in content
BannerAdView(zoneId: "YOUR_MREC_ZONE_ID", height: 250)
.frame(height: 250)
Text("Article content...")
}
}
}
}
Lifecycle Management
Zone Control
- Swift
- Objective-C
// Pause banner refresh timer
EMAManager.shared.pauseBannerZone(zoneId: "YOUR_ZONE_ID")
// Resume banner refresh timer
EMAManager.shared.resumeBannerZone(zoneId: "YOUR_ZONE_ID")
// Destroy banner zone completely
EMAManager.shared.destroyBannerZone(zoneId: "YOUR_ZONE_ID")
// Pause banner refresh timer
[[EMAManager shared] pauseBannerZone:@"YOUR_ZONE_ID"];
// Resume banner refresh timer
[[EMAManager shared] resumeBannerZone:@"YOUR_ZONE_ID"];
// Destroy banner zone completely
[[EMAManager shared] destroyBannerZone:@"YOUR_ZONE_ID"];
Banners already pause and resume automatically based on their on-screen position (see In a Scroll View), so you rarely need these. For edge cases where you want to drive visibility explicitly — e.g. a custom container whose geometry the SDK can't read reliably — signal it yourself:
- Swift
- Objective-C
EMAManager.shared.notifyBannerVisible(zoneId: "YOUR_ZONE_ID")
EMAManager.shared.notifyBannerInvisible(zoneId: "YOUR_ZONE_ID")
[[EMAManager shared] notifyBannerVisible:@"YOUR_ZONE_ID"];
[[EMAManager shared] notifyBannerInvisible:@"YOUR_ZONE_ID"];
Tab Bar Navigation
When using tab bar controllers, pause and resume banners on inactive tabs:
class TabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
func tabBarController(_ tabBarController: UITabBarController,
didSelect viewController: UIViewController) {
// Pause all banners on previously visible tabs
// Resume banners on the newly selected tab
}
}
Auto-Refresh
Banner ads automatically refresh based on server configuration. The SDK handles:
- Pausing refresh when view is not visible
- Pausing refresh when app is in background
- Resuming refresh when view becomes visible again
You don't need to manually reload banners for refresh - it's automatic.
Best Practices
1. Container Size
For fixed-size banners, set an explicit height constraint (adaptive banners size themselves — see best practice 7):
// Good
bannerContainer.heightAnchor.constraint(equalToConstant: 50).isActive = true
// Avoid - may cause layout issues
bannerContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 50).isActive = true
2. Safe Area
Position banners within safe area:
// Good - respects safe area
bannerContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
// Avoid - may overlap with home indicator
bannerContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor)
3. Load in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadBanner() // Load when view is about to appear
}
4. Handle Failures Gracefully
func bannerStatusChanged(status: AdStatus, zoneId: String, adUnitId: String) {
if case .failed = status {
// Option 1: Hide container
bannerContainer.isHidden = true
// Option 2: Show fallback content
showFallbackContent()
// Option 3: Collapse container height
bannerHeightConstraint.constant = 0
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
5. Use the Cell API in Recycling Lists
For banners in UITableViewCell / UICollectionViewCell, load with the cell-aware
loadBannerAd(cell:) (see In a List) so a recycled cell
never shows a stale ad from another row. Don't use the plain loadBannerAd for recycling cells.
6. Let the SDK Manage Visibility
Banners pause and resume themselves based on their on-screen position in any scroll container (see In a Scroll View). Don't destroy and reload a banner as it scrolls off and back on screen, and don't manually pause/resume it — the SDK already does this, and reloading on every scroll wastes requests and hurts viewability.
7. Prefer Adaptive Sizes
Anchored adaptive (.adaptiveAnchored / .adaptiveAnchoredLarge) generally earns more than a fixed
320×50 — see Adaptive & Custom Sizes. Give adaptive banners a flexible
height rather than a hard 50pt constraint.
Troubleshooting
Banner Not Showing
- Check container visibility:
print("Hidden: \(bannerContainer.isHidden)")
print("Alpha: \(bannerContainer.alpha)")
print("Frame: \(bannerContainer.frame)")
- Check container is in view hierarchy:
print("Superview: \(bannerContainer.superview)")
-
Check zone ID is correct
-
Enable debug logging:
EMASettings.shared.logLevel = .all
Banner Loading Slowly
The SDK loads ads from multiple sources in parallel and selects the best one. This optimization may take a moment. You can show a placeholder:
func bannerStatusChanged(status: AdStatus, zoneId: String, adUnitId: String) {
switch status {
case .initializing:
showPlaceholder()
case .ready:
hidePlaceholder()
default:
break
}
}
API Reference
EMAManager.loadBannerAd (Recommended)
func loadBannerAd(
viewController: UIViewController,
zoneId: String,
bannerContainer: UIView,
delegate: AdStatusDelegate? = nil,
keywords: String? = nil,
sizeMode: EMABannerSizeMode = .fixed
)
| Parameter | Type | Description |
|---|---|---|
viewController | UIViewController | The hosting view controller (for lifecycle management) |
zoneId | String | Your banner zone ID |
bannerContainer | UIView | Container view for the banner |
delegate | AdStatusDelegate? | Callback delegate |
keywords | String? | Targeting keywords |
sizeMode | EMABannerSizeMode | Per-load sizing override (default .fixed). See Adaptive & Custom Sizes. |
EMAManager.loadBannerAd (Legacy)
func loadBannerAd(
zoneId: String,
container: UIView,
delegate: AdStatusDelegate? = nil,
customParameters: String? = nil
)
| Parameter | Type | Description |
|---|---|---|
zoneId | String | Your banner zone ID |
container | UIView | Container view for the banner |
delegate | AdStatusDelegate? | Callback delegate |
customParameters | String? | Custom targeting parameters |
EMAManager.loadBannerAd (In a Cell)
func loadBannerAd(
cell: UIView,
zoneId: String,
bannerContainer: UIView,
delegate: AdStatusDelegate? = nil,
keywords: String? = nil,
sizeMode: EMABannerSizeMode = .fixed
)
Use for banners inside a UITableViewCell / UICollectionViewCell — pass the cell so the SDK
handles reuse. ObjC: loadBannerAdInCell:zoneId:container:observer:keywords:.
Zone Management
func pauseBannerZone(zoneId: String)
func resumeBannerZone(zoneId: String)
func destroyBannerZone(zoneId: String)
func notifyBannerVisible(zoneId: String) // optional explicit signal (banners auto-track by geometry)
func notifyBannerInvisible(zoneId: String) // optional explicit signal (banners auto-track by geometry)
Objective-C selectors
| Swift | Objective-C selector |
|---|---|
loadBannerAd(viewController:zoneId:bannerContainer:delegate:keywords:) | loadBannerAdWithViewController:zoneId:container:observer:keywords: |
loadBannerAd(…sizeMode:) (adaptive/custom) | loadBannerAdWithViewController:zoneId:container:observer:keywords:adaptiveMode:customWidth:customHeight: |
pauseBannerZone(zoneId:) | pauseBannerZone: |
resumeBannerZone(zoneId:) | resumeBannerZone: |
destroyBannerZone(zoneId:) | destroyBannerZone: |
notifyBannerVisible(zoneId:) | notifyBannerVisible: |
notifyBannerInvisible(zoneId:) | notifyBannerInvisible: |
In Objective-C, adopt
EMAAdObserverand pass your object as theobserver:argument. Status is delivered asEMAAdStatusTypeviabannerStatusChanged:zoneId:adUnitId:.