prebid / prebid-mobile-ios

Prebid Mobile SDK for iOS applications
Apache License 2.0
47 stars 88 forks source link

Third party rendering #697

Open YuriyVelichkoPI opened 1 year ago

YuriyVelichkoPI commented 1 year ago

Antoine Barrault posted the next feature request in the Slack channel

Hello Prebid Mobile Team,

We would like to make a proposition about the development of a new feature we are going to make.

Context

We are planning of implement a third-party renderer feature (called as rendering delegation on prebid documentation). As part of this process, we need to pass an extra information inside the Bid Request in order to inform the Bid apdaters, that a custom renderer is available on the publisher app.

The information

The Bid adapter need to have a way to know if custom renderers are available in order to be able to deliver specials creatives. A simple representation for it, is an array of strings something like: [“custom_renderer_1”, “custom_renderer_2”]. We can call it third_party_renderers inside the openRTB representation.

Solution

To pass the information to the bid adpaters we can simply use the context data and passing a new key/value tuple. Example:

TargetingParams.addContextData("third_party_renderers", "[teads]")

A more robust alternative can be to implement a new global targeting parameter like User Keywords that will allow the publisher or third party SDKs to pass new strings (the supported custom renderers) to this object.

TargetingParams.addCustomRenderer("custom_renderer_1")
github-antoine-barrault commented 1 year ago

I made a full proposal on Android repository : https://github.com/prebid/prebid-mobile-android/issues/507

github-antoine-barrault commented 1 year ago

In addition to the android proposal we add some implementations details on iOS side.

ThirdPartyCustomRenderer: a new protocol for the third-party renderer

For doing third-party rendering, SDKs will have to implement a protocol specially created for it: ThirdPartyCustomRenderer.

@objc public protocol ThirdPartyCustomRenderer: NSObjectProtocol {
    /// Call this to setup the renderer and register it as "third party renderer"
    @objc static func setup()

    @objc init()

    @objc func loadAd(with frame: CGRect, bid: PrebidMobile.Bid, configId: String, adViewDelegate: PBMAdViewDelegate?)
}

An important point of this protocol is the PBMAdViewDelegate passed on the loadAd method. It is a protocol typealias between PBMAdViewManagerDelegate (that already exists today, that is used to pass ads events) and a new protocol PBMThirdPartyAdViewLoader that has to be called when the third-party renderer did load the ad with success.

public typealias PBMAdViewDelegate = PBMThirdPartyAdViewLoader & PBMAdViewManagerDelegate

Extend DisplayViewLoadingDelegate protocol

We need to extend DisplayViewLoadingDelegate because we need to pass the third-party ad view to the adapters (PrebidAdMobBannerAdapter, PrebidMAXMediationAdapter, PBMBannerAdLoader).

@objc public protocol DisplayViewLoadingDelegate: NSObjectProtocol {

    func displayViewDidLoadAd(_ displayView: PBMDisplayView)

    func displayView(_ displayView: PBMDisplayView,
                     didFailWithError error: Error)

+    func customRenderDisplayViewDidLoadAd(_ displayView: UIView, adSize: CGSize)

}

Updates to be done inside the PBMDisplayView

To make it works we need to conform to the new protocol PBMThirdPartyAdViewLoader implementing the function:

- (void)adViewLoaded:(UIView *)adView adSize:(CGSize)adSize

That will just passed the view to the next delegate (DisplayViewLoadingDelegate).

Display a custom ad when it is needed

We choose to put the call of the third-party renderer logic inside the function - (void)displayAd of the PBMDisplayView as it is the class responsible for the ad displaying in all ad placement cases. We decide to add a new function called before the initialization of the transactionFactory. In this case the third-party renderer will be called the transactionFactory will not be created.


 - (void)displayAd {
    if (self.transactionFactory) {
        return;
    }

    self.adConfiguration.adConfiguration.winningBidAdFormat = self.bid.adFormat;

+    if ([self loadThirdPartyRendererIfNeeded]) {
+        return;
+    }

    @weakify(self);
    self.transactionFactory = [[PBMTransactionFactory alloc] initWithBid:self.bid
                                                         adConfiguration:self.adConfiguration
    }];
}

This new function will return a boolean that will be false if no custom renderers were found.


- (BOOL)loadThirdPartyRendererIfNeeded {
    NSString *bidderClass = [[self.bid targetingInfo] valueForKey:@"hb_bidder"];
    self.customRenderer = [self customRendererWithClassName:bidderClass];
        if (self.customRenderer != nil) {
            CGRect const displayFrame = CGRectMake(0, 0, self.bid.size.width, self.bid.size.height);
            [self.customRenderer loadAdWith: displayFrame bid:self.bid configId: [_adConfiguration configId] adViewDelegate:self];
            return YES;
        }
    return NO;
}

- (nullable id<ThirdPartyCustomRenderer>) customRendererWithClassName:(NSString *) className {
    Class customRenderer = NSClassFromString(className);
    return [[customRenderer alloc] init];
}

Use a singleton to stock the customs renderers

Alternatively: we can use a singleton instead of loading the ThirdPartyCustomRenderer using NSClassFromString.

We design a Singleton instance called CustomRendererStore


@objc public class CustomRendererStore: NSObject {
    @objc public static let shared = CustomRendererStore()

    private override init() {
        super.init()
    }

    public func addAdapter(_ adapter: ThirdPartyCustomRenderer, for key: String) {
        adapters[key] = adapter
    }
    @objc public var adapters: [String: ThirdPartyCustomRenderer] = [String: ThirdPartyCustomRenderer]()
    }
}

On the loadThirdPartyRendererIfNeeded method, we just need to change the way we load the customRenderer.


 - (BOOL)loadThirdPartyRendererIfNeeded {
    NSString *bidderClass = [[self.bid targetingInfo] valueForKey:@"hb_bidder"];
-   self.customRenderer = [self customRendererWithClassName:bidderClass];
+   NSObject<CustomRenderer> *adapater = [[[CustomRendererStore shared] adapters] valueForKey: bidder];  
        if (self.customRenderer != nil) {
            CGRect const displayFrame = CGRectMake(0, 0, self.bid.size.width, self.bid.size.height);
            [self.customRenderer loadAdWith: displayFrame bid:self.bid configId: [_adConfiguration configId] adViewDelegate:self];
            return YES;
        }
    return NO;
}

Also we need to make sure that the publisher will declare the custom renderer at some point before using it:

CustomRendererStore.shared.addAdapter(TeadsPrebidAdapter(), for: "teads")
jsligh commented 6 months ago

Teads has finished the Android portion and it is already merged into the Android Repo. The iOS portion is to be handed off to myself and the committee/community to finish.

jsligh commented 2 weeks ago

Waiting for confirmation of docs PR.