原生广告
概述
原生广告是一种与应用内容和设计自然融合的广告形式,开发者只需按照标准集成步骤即可在应用中无缝展示广告内容。原生广告能够与应用的界面和用户体验协调一致,不会干扰用户的正常使用。用户可以在浏览应用内容时自然地接触到这些广告,从而提高广告的接受度和互动性。开发者可以根据应用的风格和用户的需求定制广告的展示方式,以实现最佳的广告效果和用户体验。
模版渲染广告和开发者自渲染广告,统一使用 SANativeExpressAdsManager 加载
SANativeExpressAdsManager 属性及方法介绍
SANativeExpressAdsManager 代理方法
- (void)sa_nativeExpressAdsManager:(SANativeExpressAdsManager *)manager loadNativeExpressAdSuccess:(NSArray<SANativeExpressFeedAd *> *)ads;
- (void)sa_nativeExpressAdsManager:(SANativeExpressAdsManager *)manager didFailWithError:(NSError *_Nullable)error;
SANativeExpressFeedAd 属性介绍
属性 | 说明 |
---|
BOOL expressAd | 是否是原生模版渲染广告 |
SANativeExpressAdView *expressAdView | expressAd为YES时,获取模版渲染广告View |
SANativeAdCanvasView *canvasView | expressAd为NO时,获取开发者自渲染广告View |
模版渲染
由广告网络提供多种的广告模板样式,上下图文、左右图文、三小图+文等
场景常见于应用的内容信息流中与应用内容穿插展示,如资讯列表页每隔几条资讯穿插一条广告。
SANativeExpressAdView 属性及方法介绍
属性 | 说明 | 是否必填 |
---|
| 代理 | 必填 |
| 所在的viewController,用于点击事件的跳转等,弱引用 | 必填 |
方法 | 说明 |
---|
- (void)render; | 渲染广告视图,在收到方法回调后,可以获取广告视图正确的尺寸 |
- (BOOL)isReady; | 对于已经加载成功,但未及时展示的广告,在展示前用此方法判断广告是否还可用 |
| |
| |
SANativeExpressAdView 代理方法介绍
@protocol SANativeExpressAdViewDelegate <NSObject>
@optional
- (void)sa_nativeExpressAdViewRenderSuccess:(SANativeExpressAdView *)adView;
- (void)sa_nativeExpressAdView:(SANativeExpressAdView *)adView didFailWithError:(NSError *_Nullable)error;
- (void)sa_nativeExpressAdViewDidClick:(SANativeExpressAdView *)adView;
- (void)sa_nativeExpressAdViewDidClose:(SANativeExpressAdView *)adView;
- (void)sa_nativeExpressAdViewDidExposed:(SANativeExpressAdView *)adView;
@end
开发者自渲染
当广告网络提供的原生模版渲染广告的样式和展示场景无法满足需求时,可以采用开发者自渲染广告
使用自渲染的API,可以为应用打造自定义的样式和展示场景。
SANativeAdData 属性及方法介绍:
自渲染素材类SANativeAdData
属性 | 说明 |
---|
title | 标题 |
desc | 描述 |
imageUrl | 图片url |
| app icon url |
videoUrl | 视频url |
imageUrlList | 图片url数组 |
| 广告素材类型枚举 |
| 视频时长,单位秒 |
| 广告logo url |
方法 | 说明 |
---|
- (BOOL)isReady; | 对于已经加载成功,但未及时展示的广告,在展示前用此方法判断广告是否还可用 |
| |
| |
枚举 | 说明 |
---|
| 单张图片 |
| 多张图片 |
| 视频类型 |
| 没有主要的图片和视频素材,只有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
- (void)sa_nativeAdCanvasView:(SANativeAdCanvasView *)adView didFailWithError:(NSError *_Nullable)error;
- (void)sa_nativeAdCanvasViewDidClick:(SANativeAdCanvasView *)adView;
- (void)sa_nativeAdCanvasViewDidClose:(SANativeAdCanvasView *)adView;
- (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;
}
- (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 {
}
- (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 {
}
- (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;
}
}
- (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;
}
- (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
自渲染示例
@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)];
self.descLabel.frame = CGRectMake(
textX,
CGRectGetMaxY(self.titleLabel.frame) + sad_canvasSubviewPadding,
textWidth,
descSize.height
);
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