跳至主要內容

原生广告

大约 10 分钟

原生广告

概述

原生广告是一种与应用内容和设计自然融合的广告形式,开发者只需按照标准集成步骤即可在应用中无缝展示广告内容。原生广告能够与应用的界面和用户体验协调一致,不会干扰用户的正常使用。用户可以在浏览应用内容时自然地接触到这些广告,从而提高广告的接受度和互动性。开发者可以根据应用的风格和用户的需求定制广告的展示方式,以实现最佳的广告效果和用户体验。

模版渲染广告和开发者自渲染广告,统一使用 SANativeExpressAdsManager 加载

SANativeExpressAdsManager 属性及方法介绍

属性说明是否必填
必填
可选
可选
回调设置必填
方法说明
构造方法,传入广告位ID。
加载广告

SANativeExpressAdsManager 代理方法

/// 原生广告加载成功
/// - Parameters:
///   - manager: manager对象
///   - ads: 原生广告广告数组
- (void)sa_nativeExpressAdsManager:(SANativeExpressAdsManager *)manager loadNativeExpressAdSuccess:(NSArray<SANativeExpressFeedAd *> *)ads;

/// 加载模版或者自渲染原生广告失败
/// - Parameters:
///   - manager: manager对象
///   - error: error信息
- (void)sa_nativeExpressAdsManager:(SANativeExpressAdsManager *)manager didFailWithError:(NSError *_Nullable)error;

SANativeExpressFeedAd 属性介绍

属性说明
BOOL expressAd是否是原生模版渲染广告
SANativeExpressAdView *expressAdViewexpressAd为YES时,获取模版渲染广告View
SANativeAdCanvasView *canvasViewexpressAd为NO时,获取开发者自渲染广告View

模版渲染

由广告网络提供多种的广告模板样式,上下图文、左右图文、三小图+文等

场景常见于应用的内容信息流中与应用内容穿插展示,如资讯列表页每隔几条资讯穿插一条广告。

SANativeExpressAdView 属性及方法介绍

属性说明是否必填
代理必填
所在的viewController,用于点击事件的跳转等,弱引用必填
方法说明
- (void)render;渲染广告视图,在收到方法回调后,可以获取广告视图正确的尺寸
- (BOOL)isReady;对于已经加载成功,但未及时展示的广告,在展示前用此方法判断广告是否还可用

SANativeExpressAdView 代理方法介绍

@protocol SANativeExpressAdViewDelegate <NSObject>
@optional
/// 模版信息流view渲染成功
- (void)sa_nativeExpressAdViewRenderSuccess:(SANativeExpressAdView *)adView;
/// 模版信息流view各种错误信息
- (void)sa_nativeExpressAdView:(SANativeExpressAdView *)adView didFailWithError:(NSError *_Nullable)error;
/// 模版信息流view被点击
- (void)sa_nativeExpressAdViewDidClick:(SANativeExpressAdView *)adView;
/// 模版信息流view被关闭
- (void)sa_nativeExpressAdViewDidClose:(SANativeExpressAdView *)adView;
/// 模版信息流view被曝光
- (void)sa_nativeExpressAdViewDidExposed:(SANativeExpressAdView *)adView;
@end

开发者自渲染

当广告网络提供的原生模版渲染广告的样式和展示场景无法满足需求时,可以采用开发者自渲染广告

使用自渲染的API,可以为应用打造自定义的样式和展示场景。

SANativeAdData 属性及方法介绍:

自渲染素材类SANativeAdData

属性说明
title标题
desc描述
imageUrl图片url
app icon url
videoUrl视频url
imageUrlList图片url数组
广告素材类型枚举
视频时长,单位秒
广告logo url
方法说明
- (BOOL)isReady;对于已经加载成功,但未及时展示的广告,在展示前用此方法判断广告是否还可用

SANativeAdMediaMode 枚举介绍

枚举说明
单张图片
多张图片
视频类型
没有主要的图片和视频素材,只有appIcon

SANativeAdCanvasView 属性及方法介绍:

属性说明是否必填
delegate代理必填
rootViewController所在的viewController,用于点击事件的跳转等,弱引用必填
titleLabel标题label只读
descLabel描述label只读
logoView广告网络的广告logo的View只读
app icon ImageView只读
mainImageView当前广告为单张大图时,所需的ImageView只读
imageViewList当前广告为多图时,所需的ImageView数组只读
当前广告为视频时,所需的视频播放View只读
cta按钮只读
关闭按钮只读
adData广告数据对象只读
方法说明
注册可点击子视图数组
播放视频,
暂停视频,
停止视频,

SANativeAdCanvasView 代理方法介绍

@protocol SANativeAdCanvasViewDelegate <NSObject>
@optional
/// 自渲染信息流view各种错误信息
- (void)sa_nativeAdCanvasView:(SANativeAdCanvasView *)adView didFailWithError:(NSError *_Nullable)error;
/// 自渲染信息流view被点击
- (void)sa_nativeAdCanvasViewDidClick:(SANativeAdCanvasView *)adView;

/// 自渲染信息流view被关闭
- (void)sa_nativeAdCanvasViewDidClose:(SANativeAdCanvasView *)adView;

/// 自渲染信息流view被曝光
- (void)sa_nativeAdCanvasViewDidExposed:(SANativeAdCanvasView *)adView;

/// 自渲染信息流视频播放状态变化回调
- (void)sa_nativeAdCanvasView:(SANativeAdCanvasView *)adView videoPlayStateDidChange:(SAAdVideoPlayState)state;
@end

视频播放状态变化枚举SAAdVideoPlayState说明

枚举说明
视频播放
视频暂停
视频停止播放
视频播放错误

原生广告示例代码

@interface SADFeedModel : NSObject
@property (nonatomic, copy) NSString *cellId;
@property (nonatomic, strong, nullable) UIView *adView;

+ (instancetype)modelWithCellId:(NSString *)cellId adView:(nullable UIView *)adView;

@end

#define SAD_RowsNumberMultiple (15)
@interface SADNativeFeedViewController ()<UITableViewDelegate, UITableViewDataSource, SANativeExpressAdsManagerDelegate, SANativeExpressAdViewDelegate, SANativeAdCanvasViewDelegate, UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;

@property (nonatomic, strong) SANativeExpressAdsManager *adManager;
@property (nonatomic, assign) CGSize adSize;
@property (nonatomic, copy) NSString *defaultCellID;
@property (nonatomic, copy) NSString *expressAdCellID;
@property (nonatomic, copy) NSString *canvasAdCellID;

@property (nonatomic, strong) NSMutableArray<SADFeedModel *> *dataSource;

@end

@implementation SADNativeFeedViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    _dataSource = [NSMutableArray array];


    _defaultCellID = @"SADFeedMessageCellID";
    _expressAdCellID = @"SADNativeExpressAdCellID";
    _canvasAdCellID = @"SADNativeCanvasAdCellID";

    [_tableView registerNib:[UINib nibWithNibName:@"SADFeedMessageCell" bundle:nil] forCellReuseIdentifier:_defaultCellID];
    [_tableView registerClass:UITableViewCell.class forCellReuseIdentifier:_expressAdCellID];
    [_tableView registerClass:UITableViewCell.class forCellReuseIdentifier:_canvasAdCellID];
}

- (void)loadNativeExpressAds:(UIButton *)sender {
    NSString *slotId = @"id";
    _adSize = CGSizeMake(width, height);
    _adManager.adSize = CGSizeMake(width, height);
    _adManager.delegate = self;
    [_adManager loadAd];
    _logTextView.hidden = NO;
}

- (NSUInteger)expressAdIndex:(UIView *)adView {
    NSUInteger index = NSNotFound;
    for (NSUInteger i = 0; i < _dataSource.count; i ++) {
        SADFeedModel *model = _dataSource[i];
        if ([model.adView isEqual:adView]) {
            index = i;
            break;
        }
    }
    return index;
}

//MARK: - SANativeAdsManagerDelegate
- (void)sa_nativeExpressAdsManager:(SANativeExpressAdsManager *)manager loadNativeExpressAdSuccess:(NSArray<SANativeExpressFeedAd *> *)ads {
    // 加载成功
    [ads enumerateObjectsUsingBlock:^(SANativeExpressFeedAd * _Nonnull ad, NSUInteger idx, BOOL * _Nonnull stop) {
        if (ad.isExpressAd) {
            SANativeExpressAdView *adView = ad.expressAdView;
            if ([adView isReady]) {
                adView.delegate = self;
                adView.rootViewController = self;
                [adView render];
            }
            [self.dataSource addObject:[SADFeedModel modelWithCellId:self.expressAdCellID adView:adView]];
        } else {
            SANativeAdCanvasView *adView = ad.canvasView;
            if ([adView.adData isReady]) {

                adView.delegate = self;
                adView.rootViewController = self;
                if (adView.adData.mediaMode == SANativeAdMediaModeOnlyIcon) {
                    adView.frame = (CGRect) {CGPointZero, {self.adSize.width, 80}};
                } else {
                    adView.frame = (CGRect) {CGPointZero, self.adSize};
                }
                [adView customRender];

            }
            [self.dataSource addObject:[SADFeedModel modelWithCellId:self.canvasAdCellID adView:adView]];
        }
        for (int i = 0; i < SAD_RowsNumberMultiple; i ++) {
            [self.dataSource addObject:[SADFeedModel modelWithCellId:self.defaultCellID adView:nil]];
        }
    }];

    [_tableView reloadData];
}

- (void)sa_nativeExpressAdsManager:(SANativeExpressAdsManager *)manager didFailWithError:(NSError *)error {
    // 加载失败
}

//MARK: - SANativeExpressAdViewDelegate
- (void)sa_nativeExpressAdViewRenderSuccess:(SANativeExpressAdView *)adView {
    // 广告渲染成功
    NSUInteger index = [self expressAdIndex:adView];
    if (index != NSNotFound) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
        if ([_tableView numberOfRowsInSection:0] > indexPath.row) {
            [UIView performWithoutAnimation:^{
                [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            }];
        }
    }
}

- (void)sa_nativeExpressAdView:(SANativeExpressAdView *)adView didFailWithError:(NSError *)error {
    // 广告渲染失败
}

- (void)sa_nativeExpressAdViewDidClick:(SANativeExpressAdView *)adView {
}

- (void)sa_nativeExpressAdViewDidClose:(SANativeExpressAdView *)adView {
    // 广告被关闭
    NSUInteger index = [self expressAdIndex:adView];
    if (index != NSNotFound) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
        [_tableView beginUpdates];
        [_dataSource removeObjectAtIndex:index];
        [UIView performWithoutAnimation:^{
            [_tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }];
        [_tableView endUpdates];
    }
}

- (void)sa_nativeExpressAdViewDidExposed:(SANativeExpressAdView *)adView {
    // 广告曝光
}

//MARK: - SANativeAdCanvasViewDelegate
- (void)sa_nativeAdCanvasViewDidClick:(SANativeAdCanvasView *)adView {
    // 广告被点击
}

- (void)sa_nativeAdCanvasViewDidClose:(SANativeAdCanvasView *)adView {
    // 广告被关闭
    NSUInteger index = [self expressAdIndex:adView];
    if (index != NSNotFound) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
        [_tableView beginUpdates];
        [_dataSource removeObjectAtIndex:index];
        [UIView performWithoutAnimation:^{
            [_tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }];
        [_tableView endUpdates];
    }
}

- (void)sa_nativeAdCanvasViewDidExposed:(SANativeAdCanvasView *)adView {
    // 广告被曝光
}

- (void)sa_nativeAdCanvasView:(SANativeAdCanvasView *)adView videoPlayStateDidChange:(SAAdVideoPlayState)state {
    switch (state) {
        case SAAdVideoPlayStatePlaying:
            // 视频播放中
            break;
        case SAAdVideoPlayStatePause:
            // 视频暂停
            break;
        case SAAdVideoPlayStateStopped:
            // 视频停止
            break;
        case SAAdVideoPlayStateError:
            // 视频报错
            break;
        default:
            break;
    }

}

//MARK: - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _dataSource.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    SADFeedModel *model = _dataSource[indexPath.row];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:model.cellId forIndexPath:indexPath];
    [[cell.contentView viewWithTag:1001] removeFromSuperview];
    model.adView.tag = 1001;
    if ([model.adView isKindOfClass:SANativeExpressAdView.class]) {
        model.adView.frame = (CGRect) {CGPointZero, model.adView.bounds.size};
        [cell.contentView addSubview:model.adView];
    } else if ([model.adView isKindOfClass:SANativeAdCanvasView.class]){
        [cell.contentView addSubview:model.adView];
    }
    return cell;
}

//MARK: - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    SADFeedModel *model = _dataSource[indexPath.row];
    if ([model.adView isKindOfClass:SANativeExpressAdView.class]) {
        UIView *adView = model.adView;
        return adView.bounds.size.height;
    } else if ([model.adView isKindOfClass:SANativeAdCanvasView.class]){
        SANativeAdCanvasView *adView = (SANativeAdCanvasView *)model.adView;
        if (adView.adData.mediaMode == SANativeAdMediaModeOnlyIcon) {
            return 80;
        }
        return self.adSize.height;
    } else {
        return 65;
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    SADFeedModel *model = _dataSource[indexPath.row];
    if ([model.adView isKindOfClass:SANativeAdCanvasView.class]){
        SANativeAdCanvasView *adView = (SANativeAdCanvasView *)model.adView;
        if (adView.adData.mediaMode == SANativeAdMediaModeVideo) {
            [adView play];
        }
    }
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row >= _dataSource.count) {
        return;
    }
    SADFeedModel *model = _dataSource[indexPath.row];
    if ([model.adView isKindOfClass:SANativeAdCanvasView.class]){
        SANativeAdCanvasView *adView = (SANativeAdCanvasView *)model.adView;
        if (adView.adData.mediaMode == SANativeAdMediaModeVideo) {
            [adView pause];
        }
    }
}

@end

自渲染示例

/// SANativeAdCanvasView+SADCustomRender.h
@interface SANativeAdCanvasView (SADCustomRender)
- (void)customRender;
@end

#import <SDWebImage/SDWebImage.h>

static UIEdgeInsets sad_canvasViewContentInsets = (UIEdgeInsets) {10, 10, 10, 10};
static CGFloat sad_canvasSubviewPadding = 5;

@implementation SANativeAdCanvasView (SADCustomRender)

- (void)customRender {
    self.backgroundColor = UIColor.whiteColor;
    [self _basicLayout];

    switch (self.adData.mediaMode) {
        case SANativeAdMediaModeOneImage:
            [self _oneImageLayout];
            break;
        case SANativeAdMediaModeGroupImage:
            [self _groupImageLayout];
            break;
        case SANativeAdMediaModeVideo:
            [self _videoLayout];
            break;
        case SANativeAdMediaModeOnlyIcon:
            break;
        default:
            break;
    }

    NSMutableArray<UIView *> *click = [NSMutableArray array];
    if (self.mainImageView) {
        [click addObject:self.mainImageView];
    }
    if (self.videoView) {
        [click addObject:self.videoView];
    }
    if (self.imageViewList) {
        [click addObjectsFromArray:self.imageViewList];
    }
    if (self.iconImageView) {
        [click addObject:self.iconImageView];
    }
    if (self.callToActionBtn) {
        [click addObject:self.callToActionBtn];
    }
    [self registerClickableViews:click];
}


- (void)_basicLayout {

    CGFloat textX = sad_canvasViewContentInsets.left;
    if (self.iconImageView && self.adData.iconUrl.length != 0) {
        [self.iconImageView sd_setImageWithURL:[NSURL URLWithString:self.adData.iconUrl]];
        self.iconImageView.frame = CGRectMake(
            sad_canvasViewContentInsets.left,
            sad_canvasViewContentInsets.top,
            45,
            45
        );
        textX =  CGRectGetMaxX(self.iconImageView.frame) + sad_canvasSubviewPadding;
    }

    CGFloat textWidth = self.bounds.size.width - textX - sad_canvasViewContentInsets.right;
    self.titleLabel.textColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:1];
    self.titleLabel.font = [UIFont systemFontOfSize:14];
    self.titleLabel.frame = CGRectMake(
        textX,
        sad_canvasViewContentInsets.top,
        textWidth,
        [UIFont systemFontOfSize:14].lineHeight
    );

    self.descLabel.textColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1];
    self.descLabel.font = [UIFont systemFontOfSize:12];
    self.descLabel.numberOfLines = 2;
    CGFloat maxDescSize = 45 - CGRectGetHeight(self.titleLabel.frame) - sad_canvasSubviewPadding;
    CGSize descSize = [self.descLabel sizeThatFits:CGSizeMake(textWidth, maxDescSize)];
    //    if (descSize.height > 45 - CGRectGetHeight(self.titleLabel.frame) - sad_canvasSubviewPadding) {
    //
    //    }
    self.descLabel.frame = CGRectMake(
        textX,
        CGRectGetMaxY(self.titleLabel.frame) + sad_canvasSubviewPadding,
        textWidth,
        descSize.height
    );
    //    CGFloat logoWH = 25;
    if (CGRectEqualToRect(self.logoView.bounds, CGRectZero)) {
        [self.logoView sizeToFit];        
    }
    CGSize logoSize = self.logoView.bounds.size;
    self.logoView.frame = CGRectMake(
        sad_canvasViewContentInsets.left,
        self.bounds.size.height - sad_canvasViewContentInsets.bottom - logoSize.height,
        logoSize.width,
        logoSize.height
    );
    CGFloat dislikeWH = 20;
    [self.dislikeBtn setTitleColor:[UIColor colorWithRed:0.3 green:0.3 blue:0.3 alpha:1] forState:UIControlStateNormal];
    [self.dislikeBtn setTitle:@"X" forState:UIControlStateNormal];
    [self.dislikeBtn setImage:nil forState:UIControlStateNormal];
    self.dislikeBtn.frame = CGRectMake(
        self.bounds.size.width - sad_canvasViewContentInsets.right - dislikeWH,
        sad_canvasViewContentInsets.top,
        dislikeWH,
        dislikeWH
    );

    [self.callToActionBtn sizeToFit];
    CGSize ctaSize = self.callToActionBtn.bounds.size;
    self.callToActionBtn.frame = CGRectMake(
        self.bounds.size.width - sad_canvasViewContentInsets.right - ctaSize.width,
        self.bounds.size.height - sad_canvasViewContentInsets.bottom - ctaSize.height,
        ctaSize.width,
        ctaSize.height
    );

}

- (void)_oneImageLayout {
    CGFloat y = CGRectGetMaxY(self.descLabel.frame) + sad_canvasSubviewPadding;
    if (self.iconImageView) {
        y =  CGRectGetMaxY(self.iconImageView.frame) + sad_canvasSubviewPadding;
    }
    self.mainImageView.contentMode = UIViewContentModeScaleAspectFit;
    self.mainImageView.frame = CGRectMake(
        sad_canvasViewContentInsets.left,
        y,
        self.bounds.size.width - sad_canvasViewContentInsets.left - sad_canvasViewContentInsets.right,
        CGRectGetMaxY(self.logoView.frame) - y
    );
    NSURL *imageUrl = [NSURL URLWithString:self.adData.imageUrl];
    if (imageUrl) {
        [self.mainImageView sd_setImageWithURL:imageUrl];
    }
}

- (void)_groupImageLayout {
    NSUInteger count = self.imageViewList.count;
    if (count <= 0) {
        return;
    }
    CGFloat y = CGRectGetMaxY(self.descLabel.frame) + sad_canvasSubviewPadding;
    if (self.iconImageView) {
        y =  CGRectGetMaxY(self.iconImageView.frame) + sad_canvasSubviewPadding;
    }
    CGFloat groupWidth = self.bounds.size.width - sad_canvasViewContentInsets.left - sad_canvasViewContentInsets.right;
    CGFloat width = (groupWidth - (count - 1) * sad_canvasSubviewPadding) / count;
    CGFloat x = sad_canvasViewContentInsets.left;
    CGFloat height = CGRectGetMaxY(self.logoView.frame) - sad_canvasViewContentInsets.bottom - y;

    for (NSUInteger i = 0; i < count; i ++) {
        UIImageView *imageView = self.imageViewList[i];
        x = sad_canvasViewContentInsets.left + i * (width + sad_canvasSubviewPadding);
        imageView.frame = CGRectMake(x, y, height, width);
        if (self.adData.imageUrlList.count > i) {
            [imageView sd_setImageWithURL:[NSURL URLWithString:self.adData.imageUrlList[i]]];
        }
    }
}

- (void)_videoLayout {
    CGFloat y = CGRectGetMaxY(self.descLabel.frame) + sad_canvasSubviewPadding;
    if (self.iconImageView && self.adData.iconUrl.length != 0) {
        y =  CGRectGetMaxY(self.iconImageView.frame) + sad_canvasSubviewPadding;
    } else if (self.descLabel) {
        y = CGRectGetMaxY(self.descLabel.frame) + sad_canvasSubviewPadding;
    } else if (self.titleLabel) {
        y = CGRectGetMaxY(self.titleLabel.frame) + sad_canvasSubviewPadding;
    }
    self.videoView.frame = CGRectMake(
        sad_canvasViewContentInsets.left,
        y,
        self.bounds.size.width - sad_canvasViewContentInsets.left - sad_canvasViewContentInsets.right,
        CGRectGetMaxY(self.logoView.frame) - y
    );
}

@end

上次编辑于: