Skip to content

Implement iOS banner ad support#22

Merged
steffanc merged 7 commits intomainfrom
feat/ios-banners
Feb 20, 2026
Merged

Implement iOS banner ad support#22
steffanc merged 7 commits intomainfrom
feat/ios-banners

Conversation

@steffanc
Copy link
Contributor

Summary

  • Replace @NO banner stubs in the iOS plugin with full native implementation using CloudXCore API
  • Add programmatic banner positioning via Auto Layout constraints against safeAreaLayoutGuide (vertical) and physical left/right anchors (horizontal), matching AppLovin's iOS pattern
  • Implement full banner lifecycle: create, show/hide with auto-refresh management, position updates, load, destroy with proper cleanup
  • Wire up CLXBannerDelegate and CLXAdRevenueDelegate callbacks to send events (OnBannerAd*Event) back to Dart via the method channel, matching the existing Android event format
  • Handle Flutter platform channel NSNull for nullable parameters (placement, customData)
  • Stop auto-refresh on load when banner is still hidden to prevent wasted impressions (matching AppLovin behavior)

Test plan

  • flutter analyze passes clean
  • flutter run on iOS simulator — create banner, verify it renders at correct position
  • Test all 9 positions: top_left, top_center, top_right, center_left, centered, center_right, bottom_left, bottom_center, bottom_right
  • Test show/hide toggles visibility and auto-refresh correctly
  • Test updateBannerPosition moves the banner
  • Test destroyBanner cleans up the view and constraints
  • Test setBannerPlacement(null) doesn't crash (NSNull guard)
  • Verify delegate callbacks deliver events to Dart listeners

🤖 Generated with Claude Code

Replace @no banner stubs with full native implementation matching
Android behavior. Banners are created via CloudXCore API, positioned
using Auto Layout constraints against the safe area, and managed
through show/hide/destroy lifecycle with delegate callbacks sending
events back to Dart via the method channel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@steffanc
Copy link
Contributor Author

Code Review

Overall this looks solid — the implementation closely follows both the Android plugin and AppLovin's iOS patterns. A few items to consider:

Minor issues

channel property is public in the header. It only gets set once in registerWithRegistrar:. Consider readonly in .h + readwrite in the class extension to prevent external reassignment:

// .h
@property (nonatomic, strong, readonly) FlutterMethodChannel *channel;
// .m class extension
@property (nonatomic, strong, readwrite) FlutterMethodChannel *channel;

adUnitIdsToShowAfterCreate is an NSMutableArray, not a set. If showBanner is called twice before createBanner, the same ID gets added twice. Won't cause a bug (remove removes all occurrences) but wastes memory. Android uses a Set for this. Consider NSMutableSet, or guard with containsObject: before addObject:.

Future consideration

The delegate callbacks hardcode OnBannerAd*Event names. When MRECs are added (also CLXBannerAdView), didLoadAd: will need to differentiate banner vs MREC to route to the correct event name. Fine for this PR's scope, just noting for the MREC PR.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements native iOS banner ad support for the CloudX Flutter SDK, replacing stub methods that returned @NO with a complete implementation using the CloudXCore native SDK. The implementation adds banner lifecycle management (create, show, hide, destroy), programmatic positioning via Auto Layout with safe area support, delegate-based event handling, and proper resource cleanup.

Changes:

  • Added full banner ad lifecycle implementation with CloudXCore SDK integration, including programmatic positioning via Auto Layout constraints
  • Implemented CLXBannerDelegate and CLXAdRevenueDelegate callbacks to send ad events to Dart via method channel with Android-compatible event format
  • Added show-before-create queuing mechanism and auto-refresh management to prevent wasted impressions when banners are hidden

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
cloudx_flutter_sdk/ios/Classes/CloudXFlutterSdkPlugin.h Added CloudXCore import and protocol conformance (CLXBannerDelegate, CLXAdRevenueDelegate), exposed channel property
cloudx_flutter_sdk/ios/Classes/CloudXFlutterSdkPlugin.m Implemented complete banner ad support: lifecycle methods, Auto Layout positioning, delegate callbacks, event serialization, and resource management

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return;
}

CLXBannerAdView *adView = [[CloudXCore shared] createBannerWithAdUnitId:adUnitId viewController:rootViewController];
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createBannerWithAdUnitId:viewController: method is being called without checking if CloudXCore is initialized first. According to the initialization pattern in lines 52-62, CloudXCore needs to be initialized before creating ad views. Consider adding a guard to check if CloudXCore.shared.isInitialized is true, and handle the case where the SDK hasn't been initialized yet (e.g., log an error and return early).

Copilot uses AI. Check for mistakes.
Comment on lines +411 to +459
- (void)didLoadAd:(CLXAd *)ad {
NSString *adUnitId = ad.adUnitId;
NSLog(@"[CloudX Flutter] Banner loaded: %@", adUnitId);

CLXBannerAdView *adView = self.adViews[adUnitId];

// Re-position after load in case the ad view's size changed
[self positionAdViewForAdUnitIdentifier:adUnitId position:self.adViewPositions[adUnitId]];

// Do not auto-refresh if the ad view is not showing yet (e.g. first load before publisher shows banner).
// Auto-refresh will resume when showBanner is called.
if (adView && adView.hidden) {
[adView stopAutoRefresh];
}

[self sendEvent:@"OnBannerAdLoadedEvent" adUnitId:adUnitId data:@{@"ad": [self serializeAd:ad]}];
}

- (void)didFailToLoadAd:(NSString *)adUnitId error:(CLXError *)error {
NSLog(@"[CloudX Flutter] Banner load failed: %@ - %@", adUnitId, error.message);
[self sendEvent:@"OnBannerAdLoadFailedEvent" adUnitId:adUnitId data:@{@"error": [self serializeError:error]}];
}

- (void)didClickAd:(CLXAd *)ad {
NSString *adUnitId = ad.adUnitId;
NSLog(@"[CloudX Flutter] Banner clicked: %@", adUnitId);
[self sendEvent:@"OnBannerAdClickedEvent" adUnitId:adUnitId data:@{@"ad": [self serializeAd:ad]}];
}

- (void)didExpandAd:(CLXAd *)ad {
NSString *adUnitId = ad.adUnitId;
NSLog(@"[CloudX Flutter] Banner expanded: %@", adUnitId);
[self sendEvent:@"OnBannerAdExpandedEvent" adUnitId:adUnitId data:@{@"ad": [self serializeAd:ad]}];
}

- (void)didCollapseAd:(CLXAd *)ad {
NSString *adUnitId = ad.adUnitId;
NSLog(@"[CloudX Flutter] Banner collapsed: %@", adUnitId);
[self sendEvent:@"OnBannerAdCollapsedEvent" adUnitId:adUnitId data:@{@"ad": [self serializeAd:ad]}];
}

// ============================================================================
#pragma mark - CLXAdRevenueDelegate
// ============================================================================

- (void)didPayRevenueForAd:(CLXAd *)ad {
NSString *adUnitId = ad.adUnitId;
NSLog(@"[CloudX Flutter] Banner revenue paid: %@", adUnitId);
[self sendEvent:@"OnBannerAdRevenuePaidEvent" adUnitId:adUnitId data:@{@"ad": [self serializeAd:ad]}];
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delegate methods hardcode "OnBannerAd*Event" for all events. When MREC support is added in the future (currently stubbed at lines 105-116), these same delegate methods will also be called for MREC ads since both use CLXBannerDelegate. This will cause MREC events to be incorrectly sent as banner events, routing them to the wrong listener in Dart. Consider adding a mechanism to track the ad type (e.g., store format type alongside the adView, or check ad.adFormat) so the correct event prefix can be used, similar to the Android pattern at CloudXFlutterSdkPlugin.kt:813-817 which uses "On${adFormat}AdLoadedEvent".

Copilot uses AI. Check for mistakes.
[eventData addEntriesFromDictionary:data];
}

[self.channel invokeMethod:eventName arguments:eventData];
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When calling invokeMethod on the Flutter channel at line 494, there's no check to ensure self.channel is not nil. If the plugin is being torn down or the channel hasn't been set up yet, this could cause a crash. Consider adding a nil check for self.channel before invoking the method, or ensure initialization order guarantees the channel is always available when delegate methods are called.

Copilot uses AI. Check for mistakes.
- (NSDictionary *)serializeAd:(CLXAd *)ad {
return @{
@"adUnitId": ad.adUnitId ?: @"",
@"adFormat": ad.adFormat ?: @"",
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adFormat field is not being lowercased, but the Android implementation lowercases it using ad.adFormat.name.lowercase() at CloudXFlutterSdkPlugin.kt:769. This inconsistency could cause issues in the Dart layer if it expects a specific format. Consider lowercasing the adFormat value to match the Android implementation, for example: adFormat.lowercaseString or ensuring the CloudXCore SDK returns it in the expected format.

Suggested change
@"adFormat": ad.adFormat ?: @"",
@"adFormat": ad.adFormat ? [ad.adFormat lowercaseString] : @"",

Copilot uses AI. Check for mistakes.
Comment on lines +394 to +401
if ([position hasSuffix:@"_left"]) {
[constraints addObject:[adView.leftAnchor constraintEqualToAnchor:superview.leftAnchor]];
} else if ([position hasSuffix:@"_right"]) {
[constraints addObject:[adView.rightAnchor constraintEqualToAnchor:superview.rightAnchor]];
} else {
// top_center, bottom_center, centered
[constraints addObject:[adView.centerXAnchor constraintEqualToAnchor:safeArea.centerXAnchor]];
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an inconsistency in the anchor usage for positioning. Vertical positioning uses safeArea anchors for all cases (lines 384, 386, 389), but horizontal positioning uses superview anchors for left/right (lines 395, 397) and safeArea for center (line 400). For left/right positioning, consider using safeArea anchors to respect safe area insets (e.g., notches, rounded corners), or use superview anchors consistently if the intention is to ignore safe areas for horizontal edges. The comment at line 392-393 suggests physical positioning is desired, but this should still respect safe areas to avoid content being clipped by device features.

Copilot uses AI. Check for mistakes.
steffanc and others added 6 commits February 19, 2026 21:44
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unnecessary nil guard on adFormat (nonnull)
- Fix revenue: NSNumber* nullable, not primitive — use ?: @0
- Fix error.message → error.localizedDescription (no message property on CLXError)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use umbrella header CloudXCore.h (resolves module import for delegates)
- Convert CLXAdFormat enum to string for NSDictionary serialization
- Fix error.message → error.localizedDescription in NSLog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without explicit width/height constraints, Auto Layout collapses
the view to zero size — ad renders visually but touches pass through.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hannel

- Use static singleton to preserve ad state across Flutter hot restarts
- Use adEventPrefixes dictionary for format-aware event names (Banner/MRec/etc)
- Remove unused FlutterStreamHandler and EventChannel scaffolding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@steffanc steffanc merged commit 07936ee into main Feb 20, 2026
2 checks passed
@steffanc steffanc deleted the feat/ios-banners branch February 20, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants