Native
Learn how to add and configure native placements and implement additional features through the SDK interface.
Key features
The Buzzvil SDK's native inventory displays ads customized for your app that blend naturally into its screens. After a user engages with an ad, the next ad appears automatically, continuously serving ads in a single placement to accelerate app monetization. It also provides dynamic features that encourage users to engage with more ads.

Integration
Follow these steps to integrate native placements into your app.
Step 1. Prepare
- Complete Getting started.
- Obtain a Unit ID for the native placement (referred to below as
YOUR_NATIVE_UNIT_ID).
Step 2. Configure the ad layout
See the illustration and table below for the components and UI of a native ad layout.

| Component | Description |
|---|---|
| Ad title (required) | The ad's title. We recommend no more than 10 characters. You can replace part of the text with an ellipsis as needed, regardless of character count. |
| Ad creative (required) | The ad's image, video, or other creative. You must preserve its aspect ratio., Display it using BZVMediaView., Size: 1,200 x 627 px, You can add margins. Contact your Buzzvil account manager at help@buzzvil.com for details. |
| Ad description (required) | A detailed description of the ad. We recommend no more than 40 characters. You can replace part of the text with an ellipsis as needed, regardless of character count. |
| Advertiser icon (required) | The advertiser's icon image. You must preserve its aspect ratio., Size: 80 x 80 px |
| CTA button (required) | Encourages ad participation. A maximum of 7 characters is recommended; truncate with an ellipsis if needed., Display it using BZVDefaultCtaView., See Primary color for colors and Reward icon for icons. For other changes, see Implement a custom CTA button. |
BenefitHub entry point (BZVNativeToFeedView) (required) | A UI (BZVNativeToFeedView) that opens BenefitHub when tapped. Add it below the NativeAd2View layout. |
Build the native ad layout in a view controller or custom view according to the structure below. Follow these steps to implement a native placement.
The following example configures a view controller according to the native ad layout specifications.
import UIKit
import BuzzvilSDK
final class ViewController: UIViewController {
private let nativeAd2View = BZVNativeAd2View(frame: .zero)
private let mediaView = BZVMediaView(frame: .zero)
private let iconImageView = UIImageView(frame: .zero)
private let titleLabel = UILabel(frame: .zero)
private let descriptionLabel = UILabel(frame: .zero)
private let ctaView = BZVDefaultCtaView(frame: .zero)
private let nativeToFeedView = BZVNativeToFeedView(frame: .zero)
private lazy var viewBinder = BZVNativeAd2ViewBinder
.Builder(unitId: "YOUR_NATIVE_UNIT_ID")
.nativeAd2View(nativeAd2View)
.mediaView(mediaView)
.iconImageView(iconImageView)
.titleLabel(titleLabel)
.descriptionLabel(descriptionLabel)
.ctaView(ctaView)
.build()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(nativeAd2View)
nativeAd2View.addSubview(mediaView)
nativeAd2View.addSubview(iconImageView)
nativeAd2View.addSubview(titleLabel)
nativeAd2View.addSubview(descriptionLabel)
nativeAd2View.addSubview(ctaView)
self.view.addSubView(nativeToFeedView)
nativeToFeedView.setUnitId("YOUR_NATIVE_UNIT_ID")
// AutoLayout Constraints 설정
// ...
}
}
@import BuzzvilSDK;
@interface ViewController ()
@property (nonatomic, strong, readonly) BZVNativeAd2View *nativeAd2View;
@property (nonatomic, strong, readonly) BZVMediaView *mediaView;
@property (nonatomic, strong, readonly) UIImageView *iconImageView;
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) UILabel *descriptionLabel;
@property (nonatomic, strong, readonly) BZVDefaultCtaView *ctaView;
@property (nonatomic, strong, readonly) BZVNativeAd2ViewBinder *viewBinder;
@property (nonatomic, strong, readonly) BZVNativeToFeedView *nativeToFeedView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_nativeAd2View = [[BZVNativeAd2View alloc] initWithFrame:CGRectZero];
[self.view addSubview:_nativeAd2View];
_mediaView = [[BZVMediaView alloc] initWithFrame:CGRectZero];
[_nativeAd2View addSubview:_mediaView];
_iconImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
[_nativeAd2View addSubview:_iconImageView];
_titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[_nativeAd2View addSubview:_titleLabel];
_descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[_nativeAd2View addSubview:_descriptionLabel];
_ctaView = [[BZVDefaultCtaView alloc] initWithFrame:CGRectZero];
[_nativeAd2View addSubview:_ctaView];
_viewBinder = [BZVNativeAd2ViewBinder viewBinderWithBlock:^(BZVNativeAd2ViewBinderBuilder * _Nonnull builder) {
builder.unitId = @"YOUR_NATIVE_UNIT_ID";
builder.nativeAd2View = self.nativeAd2View;
builder.mediaView = self.mediaView;
builder.iconImageView = self.iconImageView;
builder.titleLabel = self.titleLabel;
builder.descriptionLabel = self.descriptionLabel;
builder.ctaView = self.ctaView;
}];
_nativeToFeedView = [[BZVNativeToFeedView alloc] initWithFrame:CGRectZero];
[self.view addSubview:_nativeToFeedView];
[_nativeToFeedView setWithUnitId:@"YOUR_NATIVE_UNIT_ID"];
// AutoLayout Constraints 설정
// ...
}
@end
Step 3. Display ads
Follow these steps and the sample code to display ads in the ad layout.
- Call
bind()on the newBZVNativeAd2ViewBinderobject to allocate and display an ad automatically.- If allocation fails, no ad appears. See step 2 to define failure behavior.
- When the user completes participation, the placement automatically refreshes to the next ad.
- Optionally pass
onRequest,onNext,onErrorandonCompletedclosures tosubscribeEvents()to implement UI or other behavior for each request state.- Call
subscribeEvents()before callingbind(). - Once configured, the same closures are used for automatic refreshes.
- Call
final class ViewController: UIViewController {
// ...생략...
private let activityIndicatorView = UIActivityIndicatorView(frame: .zero)
override func viewDidLoad() {
// ...생략...
// Warning: retain cycle 방지를 위해 closure 내에서 반드시 weak self를 사용해주세요.
viewBinder.subscribeEvents(onRequest: { [weak self] in
// 광고 할당을 요청한 상태입니다.
// 이후에는 onNext, onCompleted, onError 중 하나가 호출됩니다.
// 광고 자동 갱신을 시도할 때마다 반복적으로 호출됩니다.
// 로딩 화면 등을 구현할 수 있습니다.
self?.activityIndicatorView.startAnimating()
}, onNext: { [weak self] nativeAd2 in
// 광고 할당에 성공하면 호출됩니다.
// 이후에 광고 갱신 시 onRequest가 다시 호출됩니다.
// 광고 자동 갱신을 성공할 때마다 반복적으로 호출됩니다.
// 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
self?.activityIndicatorView.stopAnimating()
}, onError: { [weak self] error in
// 최초 광고 할당에 실패하면 호출됩니다.
// 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
self?.activityIndicatorView.stopAnimating()
// NativeAd2View를 숨기거나, Error UI로 대체할 수 있습니다.
print("Failed to load ad by \(error.localizedDescription).")
}, onCompleted: { [weak self] in
// 더 이상 갱신할 수 있는 광고가 없을 때 호출됩니다.
// 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
self?.activityIndicatorView.stopAnimating()
})
// 광고 할당 및 표시를 자동으로 수행합니다.
viewBinder.bind()
}
}
@interface ViewController ()
// ...생략...
@property (nonatomic, strong, readonly) UIActivityIndicatorView *activityIndicatorView;
@end
@implementation ViewController
- (void)viewDidLoad {
// ...생략...
// Warning: retain cycle 방지를 위해 weak self를 사용해주세요.
__weak typeof(self) weakSelf = self;
[_viewBinder subscribeEventsOnRequest:^{
// 광고 할당을 요청하면 호출됩니다.
// 이후에는 onNext, onCompleted, onError 중 하나가 호출됩니다.
// 광고 자동 갱신을 시도할 때마다 반복적으로 호출됩니다.
// 로딩 화면 등을 구현할 수 있습니다.
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView startAnimating];
}
} onNext:^(BZVNativeAd2 * _Nonnull nativeAd2) {
// 광고 할당에 성공하면 호출됩니다.
// 이후에 광고 갱신 시 onRequest가 다시 호출됩니다.
// 광고 자동 갱신을 성공할 때마다 반복적으로 호출됩니다.
// 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView stopAnimating];
}
} onError:^(NSError * _Nonnull error) {
// 최초 광고 할당에 실패하면 호출됩니다.
// 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView stopAnimating];
}
// NativeAd2View를 숨기거나, Error UI로 대체할 수 있습니다.
NSLog(@"Failed to load ad by %@.", error.localizedDescription);
} onCompleted:^{
// 더 이상 갱신할 수 있는 광고가 없을 때 호출됩니다.
// 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView stopAnimating];
}
}];
// 광고 할당 및 표시를 자동으로 수행합니다.
[_viewBinder bind];
}
@end
Display the base reward amount
Display the base reward (BaseReward) earned simply by entering BenefitHub in BZVNativeToFeedView to encourage clicks.

The following example displays base reward points in BZVNativeToFeedView.
BuzzAdBenefit.getAvailableFeedBaseReward(for: YOUR_FEED_UNIT_ID) { [weak self] (reward) in
guard let self = self else { return }
if reward > 0 {
self.nativeToFeedLabel.text = "\(reward) 포인트 받고 더 많은 참여 기회 보기"
} else {
self.nativeToFeedLabel.text = "더 많은 참여 기회 보기"
}
}
__weak typeof(self) weakSelf = self;
[BuzzAdBenefit getAvailableFeedBaseRewardFor:@"YOUR_FEED_UNIT_ID" onComplete:^(NSInteger reward) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
if (reward > 0) {
strongSelf.nativeToFeedLabel.text = [NSString stringWithFormat:@"%d 포인트 받고 더 많은 참여 기회 보기", reward];
} else {
strongSelf.nativeToFeedLabel.text = @"더 많은 참여 기회 보기";
}
}
}];
Step 4. Test the basic integration
After completing the basic integration, verify that ads are allocated and displayed correctly. Also check that NativeToFeedLayout (the layout in the native placement that opens BenefitHub) works and opens BenefitHub when clicked.
Ad operation policy (mandatory requirements)
1. Do not refresh ads
- Make the initial ad request only once when the app screen opens.
- Subsequent ad requests (reallocations) are allowed only after a clear user action, such as a screen transition.
- Automatically refreshing ads at fixed intervals or continuously requesting new ads is prohibited.
Excessive ad requests can harm the user experience, ad performance, and revenue.
2. Do not selectively use only impression-based ads
- Buzzvil combines different ad types to provide an optimized ad experience.
- API integrations must not selectively use only a specific ad type, such as impression-based ads.
- SDK integrations must not request ads from another platform when impression-based ads are unavailable.
- Operate all ad types in balance according to Buzzvil's ad logic.
Restricting or selectively using ad types negatively affects Buzzvil's ad operation strategy and revenue optimization.
Additional features
This section explains how to implement additional native placement features.
Use a native overlay
Native overlays require no separate integration or implementation. When a user returns to your app after participating in a native ad, an overlay appears over the completed ad to encourage entry into BenefitHub. The overlay contains text and an icon, covering the entire native area (NativeAdView) to encourage a click. It disappears automatically when the user clicks Close, or clicks the overlay and then returns.

Overlay UIs adapt to the native placement's size. To increase BenefitHub entry rates, the overlay shows different ad or content product UIs based on the user's past participation and preferences. Each UI type opens the following landing page.
- Daily Reward: BenefitHub's Daily Reward bottom sheet
- This does not appear unless Daily Reward is enabled in BenefitHub.
- See here for details about Daily Reward.
- Quiz rewards: BenefitHub's content rewards filter
- Fortune-telling rewards: BenefitHub's content rewards filter
- Fortune Cookie: BenefitHub's content rewards filter
- Base reward: BenefitHub
- This does not appear unless base rewards are enabled in BenefitHub.
- Total available rewards: BenefitHub
- Default UI: BenefitHub
- Appears when a server communication error occurs.

Register a video ad listener
Receive callbacks for video ad events.
The following example registers a video ad event listener on BZVNativeAd2View.
import UIKit
import BuzzvilSDK
final class ViewController: UIViewController {
private let nativeAd2View = BZVNativeAd2View(frame: .zero)
// ...생략...
override func viewDidLoad() {
// ...생략...
nativeAd2View.videoDelegate = self
}
}
// MARK: BZVNativeAdViewVideoDelegate
extension ViewController: BZVNativeAdViewVideoDelegate {
func bzvNativeAdView(_ nativeAdView: BZVNativeAdView, willStartPlayingVideoAd nativeAd: BZVNativeAd) {
// 비디오 광고의 비디오가 시작하기 직전에 호출됩니다.
}
func bzvNativeAdView(_ nativeAdView: BZVNativeAdView, didResumeVideoAd nativeAd: BZVNativeAd) {
// 비디오 광고의 비디오가 재생되면 호출됩니다.
}
func bzvNativeAdView(_ nativeAdView: BZVNativeAdView, willReplayVideoAd nativeAd: BZVNativeAd) {
// 비디오 광고의 비디오가 리플레이되면 호출됩니다.
}
func bzvNativeAdView(_ nativeAdView: BZVNativeAdView, didPauseVideoAd nativeAd: BZVNativeAd) {
// 비디오 광고의 비디오가 일시정지되면 호출됩니다.
}
func bzvNativeAdView(_ nativeAdView: BZVNativeAdView, didFinishPlayingVideoAd nativeAd: BZVNativeAd) {
// 비디오 광고의 비디오가 종료되면 호출됩니다.
}
}
@import BuzzvilSDK;
@interface ViewController () <BZVNativeAdViewVideoDelegate>
@property (nonatomic, strong, readonly) BZVNativeAd2View *nativeAd2View;
// ...생략...
@end
@implementation ViewController
- (void)viewDidLoad {
// ...생략...
_nativeAd2View.videoDelegate = self;
}
#pragma mark - BZVNativeAdViewVideoDelegate
- (void)BZVNativeAdView:(BZVNativeAdView *)nativeAdView willStartPlayingVideoAd:(BZVNativeAd *)nativeAd {
// 비디오 광고의 비디오가 시작하기 직전에 호출됩니다.
}
- (void)BZVNativeAdView:(BZVNativeAdView *)nativeAdView didResumeVideoAd:(BZVNativeAd *)nativeAd {
// 비디오 광고의 비디오가 재생되면 호출됩니다.
}
- (void)BZVNativeAdView:(BZVNativeAdView *)nativeAdView willReplayVideoAd:(BZVNativeAd *)nativeAd {
// 비디오 광고의 비디오가 리플레이되면 호출됩니다.
}
- (void)BZVNativeAdView:(BZVNativeAdView *)nativeAdView didPauseVideoAd:(BZVNativeAd *)nativeAd {
// 비디오 광고의 비디오가 일시정지되면 호출됩니다.
}
- (void)BZVNativeAdView:(BZVNativeAdView *)nativeAdView didFinishPlayingVideoAd:(BZVNativeAd *)nativeAd {
// 비디오 광고의 비디오가 종료되면 호출됩니다.
}
@end
Register an ad event listener
Receive ad callback events through BZVNativeAd2ViewBinder.subscribeAdEvents(). Call subscribeAdEvents() before calling bind() to receive them.
The following example receives ad events in BZVNativeAd2ViewBinder.
final class ViewController: UIViewController {
// ...생략...
override func viewDidLoad() {
// ...생략...
// Native 광고 이벤트 처리를 위한 closure를 등록하고 각 이벤트에 따라 필요한 기능을 구현합니다.
// 로그 기록, 단순 알림 외에 다른 동작을 추가하는 것을 권장하지 않습니다. 자동 갱신 등 네이티브 2.0의 기능과 직접 구현한 동작이 충돌할 수 있습니다.
// Warning: 반드시 bind 함수를 호출하기 전에 호출해야 해야합니다.
// Warning: retain cycle 방지를 위해 closure 내에서 반드시 weak self를 사용해주세요.
viewBinder.subscribeAdEvents(onImpressed: { [weak self] nativeAd2 in
// Native 광고가 유저에게 노출되었을 때 호출됩니다.
}, onClicked: { [weak self] nativeAd2 in
// 유저가 Native 광고를 클릭했을 때 호출됩니다.
}, onRewardRequested: { [weak self] nativeAd2 in
// 리워드 적립 요청시에 호출됩니다.
}, onRewarded: { [weak self] nativeAd2, rewardResult in
// 리워드 적립 결과를 수신했을 때 호출됩니다.
}, onParticipated: { [weak self] nativeAd2 in
// 광고 참여가 완료되었을 때 호출됩니다.
})
viewBinder.bind()
}
}
@implementation ViewController
- (void)viewDidLoad {
// ...생략...
// Native 광고 이벤트 처리를 위한 closure를 등록하고 각 이벤트에 따라 필요한 기능을 구현합니다.
// 로그 기록, 단순 알림 외에 다른 동작을 추가하는 것을 권장하지 않습니다. 자동 갱신 등 네이티브 2.0의 기능과 직접 구현한 동작이 충돌할 수 있습니다.
// Warning: 반드시 bind 함수를 호출하기 전에 호출해야 해야합니다.
// Warning: retain cycle 방지를 위해 closure 내에서 반드시 weak self를 사용해주세요.
__weak typeof(self) weakSelf = self;
[_viewBinder subscribeAdEventsOnImpressed:^(BZVNativeAd2 * _Nonnull nativeAd2) {
__strong typeof(self) strongSelf = weakSelf;
// Native 광고가 유저에게 노출되었을 때 호출됩니다.
} onClicked:^(BZVNativeAd2 * _Nonnull nativeAd2) {
__strong typeof(self) strongSelf = weakSelf;
// 유저가 Native 광고를 클릭했을 때 호출됩니다.
} onRewardRequested:^(BZVNativeAd2 * _Nonnull nativeAd2) {
__strong typeof(self) strongSelf = weakSelf;
// 리워드 적립 요청시에 호출됩니다.
} onRewarded:^(BZVNativeAd2 * _Nonnull nativeAd2, BZVRewardResult rewardResult) {
__strong typeof(self) strongSelf = weakSelf;
// 리워드 적립 결과를 수신했을 때 호출됩니다.
} onParticipated:^(BZVNativeAd2 * _Nonnull nativeAd2) {
__strong typeof(self) strongSelf = weakSelf;
// 광고 참여가 완료되었을 때 호출됩니다.
}];
[_viewBinder bind];
}
@end
Reward result types (BZVRewardResult)
| BZVRewardResult | Result | Description |
|---|---|---|
success | Success | The reward was credited successfully. |
alreadyParticipated | Failure | The reward for this ad has already been credited. |
missingReward | Failure | The ad has no reward. |
tooShortToParticipate | Failure | The user returned from the ad landing page too quickly. |
timeout | Failure | A network timeout occurred. |
browserNotLaunched | Failure | The browser did not launch to display the ad landing page. |
networkError | Failure | An unknown network error occurred. |
serverError | Failure | An unknown server error occurred. |
clientError | Failure | An unknown client error occurred. |
unknownError | Failure | An unknown error occurred. |
Add clickable areas
In addition to the default clickable MediaView and CtaView, add more clickable views using clickableViews of BZVNativeAd2ViewBinderBuilder, as shown below.
private lazy var viewBinder = BZVNativeAd2ViewBinder
.Builder(unitId: "YOUR_NATIVE_UNIT_ID")
.nativeAd2View(nativeAd2View)
.mediaView(mediaView)
.iconImageView(iconImageView)
.titleLabel(titleLabel)
.descriptionLabel(descriptionLabel)
.ctaView(ctaView)
.setClickableViews([ // (optional) 클릭 가능한 영역을 설정 할 수 있습니다.
mediaView,
ctaView
])
.build()
_viewBinder = [BZVNativeAd2ViewBinder viewBinderWithBlock:^(BZVNativeAd2ViewBinderBuilder * _Nonnull builder) {
builder.unitId = @"YOUR_NATIVE_UNIT_ID";
builder.nativeAd2View = self.nativeAd2View;
builder.mediaView = self.mediaView;
builder.iconImageView = self.iconImageView;
builder.titleLabel = self.titleLabel;
builder.descriptionLabel = self.descriptionLabel;
builder.ctaView = self.ctaView;
builder.clickableViews = @[self.mediaView, self.ctaView]; // (optional) 클릭 가능한 영역을 설정 할 수 있습니다.
}];
Implement a carousel
A carousel is a UI that displays multiple ads through horizontal scrolling. Use the Buzzvil SDK to create a carousel in a native placement and implement its required features. Optional features can further improve the user experience.

Implement basic carousel features
Step 1. Allocate ads
To allocate multiple ads in a native placement, use BZVNativeAd2Pool, which controls duplicate ad allocation.
Call BZVNativeAd2Pool.loadAds() to check how many ads can currently be allocated.
import UIKit
import BuzzvilSDK
final class CarouselViewController: UIViewController {
private let unitID = "YOUR_UNIT_ID"
// 최대 10개의 광고를 요청할 수 있습니다.
private let adRequestCount = 5
// 실제 할당받은 광고의 개수입니다.
// 이 값은 collectionView datasource의 collectionView(_:numberOfItemsInSection:) 함수의 return 값으로 사용합니다.
private var loadedAdCount = 0
// 광고 중복 할당을 막기 위해 하나의 캐러셀에 하나의 NativeAd2Pool 인스턴스를 생성하여 사용합니다.
private lazy var pool = BZVNativeAd2Pool(unitId: unitId)
override func viewDidLoad() {
super.viewDidLoad()
setupCarousel()
}
private func setupCarousel() {
// BZVNativeAd2Pool에 광고 할당을 요청합니다.
pool.loadAds(count: adRequestCount) { [weak self] adCount in
// 실제로 할당받은 광고의 개수(adCount)를 업데이트합니다.
self?.loadedAdCount = adCount
} errorHandler: { [weak self] error in
// 광고 할당 실패 시 발생하는 NSError 오류 코드에 대한 자세한 내용은 오류 코드가 나타납니다 토픽을 참고하세요.
}
}
}
@import BuzzvilSDK;
static NSString * const kUnitId = @"YOUR_UNIT_ID";
@interface CarouselViewController ()
// 최대 10개의 광고를 요청할 수 있습니다.
@property (nonatomic, assign, readonly) NSInteger adRequestCount;
// 실제 할당받은 광고의 개수입니다.
// 이 값은 collectionView datasource의 collectionView(_:numberOfItemsInSection:) 함수의 return 값으로 사용합니다.
@property (nonatomic, assign, readwrite) NSInteger loadedAdCount;
// 광고 중복 할당을 막기 위해 하나의 캐러셀에 하나의 NativeAd2Pool 인스턴스를 생성하여 사용합니다.
@property (nonatomic, strong, readonly) BZVNativeAd2Pool *pool;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
_adRequestCount = 5;
_loadedAdCount = 0;
_pool = [[BZVNativeAd2Pool alloc] initWithUnitId:kUnitId];
[self setupCarousel];
}
- (void)setupCarousel {
__weak typeof(self) weakSelf = self;
// BZVNativeAd2Pool에 광고 할당을 요청합니다.
[self.pool loadAdsWithCount:self.adRequestCount completionHandler:^(NSInteger adCount) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// 실제로 할당받은 광고의 개수(adCount)를 업데이트합니다.
strongSelf.loadedAdCount = adCount;
}
} errorHandler:^(NSError * _Nonnull error) {
// 광고 할당 실패 시 발생하는 NSError 오류 코드에 대한 자세한 내용은 오류 코드가 나타납니다 토픽을 참고하세요.
}];
}
@end
Step 2. Implement UICollectionViewCell
Use UICollectionView to arrange item views horizontally in a Native carousel.
Follow these steps to create a UICollectionViewCell for use in UICollectionView.
- Declare
CarouselCellas a subclass ofUICollectionViewCell. Configure the ad UI by following Configure the ad layout.
import UIKit
import BuzzvilSDK
final class CarouselCell: UICollectionViewCell {
private lazy var nativeAd2View = BZVNativeAd2View(frame: .zero)
private lazy var mediaView = BZVMediaView(frame: .zero)
private lazy var iconImageView = UIImageView(frame: .zero)
private lazy var titleLabel = UILabel(frame: .zero)
private lazy var descriptionLabel = UILabel(frame: .zero)
private lazy var ctaView = BZVDefaultCtaView(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupLayout()
}
required init?(coder: NSCoder) {
fatalError()
}
private func setupView() {
contentView.addSubview(nativeAd2View)
nativeAd2View.addSubview(mediaView)
nativeAd2View.addSubview(iconImageView)
nativeAd2View.addSubview(titleLabel)
nativeAd2View.addSubview(descriptionLabel)
nativeAd2View.addSubview(ctaView)
}
private func setupLayout() {
// eg. auto layout constraints for nativeAd2View
nativeAd2View.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nativeAd2View.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
nativeAd2View.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16),
nativeAd2View.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
nativeAd2View.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
])
// AutoLayout Constraints를 설정하세요.
// ...
}
}
@import BuzzvilSDK;
@interface CarouselCell ()
@property (nonatomic, strong, readonly) BZVNativeAd2View *nativeAd2View;
@property (nonatomic, strong, readonly) BZVMediaView *mediaView;
@property (nonatomic, strong, readonly) UIImageView *iconImageView;
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) UILabel *descriptionLabel;
@property (nonatomic, strong, readonly) BZVDefaultCtaView *ctaView;
@end
@implementation CarouselCell
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupView];
[self setupLayout];
}
return self;
}
- (void)setupView {
_nativeAd2View = [[BZVNativeAd2View alloc] initWithFrame:CGRectZero];
[self.contentView addSubview:_nativeAd2View];
_mediaView = [[BZVMediaView alloc] initWithFrame:CGRectZero];
[self.nativeAd2View addSubview:_mediaView];
_iconImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
[self.nativeAd2View addSubview:_iconImageView];
_titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[self.nativeAd2View addSubview:_titleLabel];
_descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[self.nativeAd2View addSubview:_descriptionLabel];
_ctaView = [[BZVDefaultCtaView alloc] initWithFrame:CGRectZero];
[self.nativeAd2View addSubview:_ctaView];
}
- (void)setupLayout {
// eg. auto layout constraints for nativeAd2View
_nativeAd2View.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_nativeAd2View.topAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.topAnchor],
[_nativeAd2View.leadingAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.leadingAnchor constant:16],
[_nativeAd2View.trailingAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.trailingAnchor constant:-16],
[_nativeAd2View.bottomAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.bottomAnchor],
]];
// AutoLayout Constraints를 설정하세요.
// ...
}
@end
- Declare
BZVNativeAd2ViewBinderinCarouselCell. ConfigureBZVNativeAd2View,BZVMediaView, and every view needed to display an ad, then create aBZVNativeAd2ViewBinderobject.
final class CarouselCell: UICollectionViewCell {
private lazy var nativeAd2View = BZVNativeAd2View(frame: .zero)
private lazy var mediaView = BZVMediaView(frame: .zero)
private lazy var iconImageView = UIImageView(frame: .zero)
private lazy var titleLabel = UILabel(frame: .zero)
private lazy var descriptionLabel = UILabel(frame: .zero)
private lazy var ctaView = BZVDefaultCtaView(frame: .zero)
// NativeAd2View와 하위 컴포넌트를 연결합니다.
private lazy var viewBinder = BZVNativeAd2ViewBinder
.Builder(unitId: "YOUR_NATIVE_UNIT_ID")
.nativeAd2View(nativeAd2View)
.mediaView(mediaView)
.iconImageView(iconImageView)
.titleLabel(titleLabel)
.descriptionLabel(descriptionLabel)
.ctaView(ctaView)
.build()
// ...
}
@interface CarouselCell ()
@property (nonatomic, strong, readonly) BZVNativeAd2View *nativeAd2View;
@property (nonatomic, strong, readonly) BZVMediaView *mediaView;
@property (nonatomic, strong, readonly) UIImageView *iconImageView;
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) UILabel *descriptionLabel;
@property (nonatomic, strong, readonly) BZVDefaultCtaView *ctaView;
@property (nonatomic, strong, readonly) BZVNativeAd2ViewBinder *viewBinder;
@end
@implementation CarouselCell
// ...
- (void)setupView {
// ...
// NativeAd2View와 하위 컴포넌트를 연결합니다.
[BZVNativeAd2ViewBinder viewBinderWithBlock:^(BZVNativeAd2ViewBinderBuilder * _Nonnull builder) {
builder.unitId = @"YOUR_NATIVE_UNIT_ID";
builder.nativeAd2View = self.nativeAd2View;
builder.mediaView = self.mediaView;
builder.iconImageView = self.iconImageView;
builder.titleLabel = self.titleLabel;
builder.descriptionLabel = self.descriptionLabel;
builder.ctaView = self.ctaView;
}];
}
// ...
@end
- Implement
setPool()andbind()inCarouselCellto configure cell data for theUICollectionViewindex. Callunbind()inprepareForReuse()so previous ad data does not remain when a cell is reused.
final class CarouselCell: UICollectionViewCell {
// ...
private lazy var viewBinder = BZVNativeAd2ViewBinder
.Builder(unitId: "YOUR_NATIVE_UNIT_ID")
.nativeAd2View(nativeAd2View)
.mediaView(mediaView)
.iconImageView(iconImageView)
.titleLabel(titleLabel)
.descriptionLabel(descriptionLabel)
.ctaView(ctaView)
.build()
// ...
override func prepareForReuse() {
super.prepareForReuse()
// prepareForReuse 내에서 unbind를 반드시 호출하여 cell을 재사용할 때 문제가 발생하지 않게 합니다.
viewBinder.unbind()
}
// collectionView의 collectionView(_:cellForItemAt:) 시점에 호출합니다.
func setPool(with pool: BZVNativeAd2Pool, for adKey: Int) {
// 해당 index(adKey)에 해당하는 NativeAd2ViewBinder가 NativeAd2Pool을 사용하도록 합니다.
viewBinder.setPool(pool, at: adKey)
}
// collectionView의 collectionView(_:cellForItemAt:) 시점에 호출합니다.
func bind() {
// NativeAd2ViewBinder의 bind()를 호출하면 광고 할당 및 갱신이 자동으로 수행됩니다.
viewBinder.bind()
}
}
@interface CarouselCell : UICollectionViewCell
- (void)setPool:(BZVNativeAd2Pool *)pool forAdKey:(NSInteger)adKey;
- (void)bind;
@end
@interface CarouselCell ()
// ...
@property (nonatomic, strong, readonly) BZVNativeAd2ViewBinder *viewBinder;
@end
@implementation CarouselCell
// ...
- (void)prepareForReuse {
[super prepareForReuse];
// prepareForReuse 내에서 unbind를 반드시 호출하여 cell을 재사용할 때 문제가 발생하지 않게 합니다.
[_viewBinder unbind];
}
// collectionView의 collectionView(_:cellForItemAt:) 시점에 호출합니다.
- (void)setPool:(BZVNativeAd2Pool *)pool forAdKey:(NSInteger)adKey {
// 해당 index(adKey)에 해당하는 NativeAd2ViewBinder가 NativeAd2Pool을 사용하도록 합니다.
[_viewBinder setPool:pool at:adKey];
}
// collectionView의 collectionView(_:cellForItemAt:) 시점에 호출합니다.
- (void)bind {
// NativeAd2ViewBinder의 bind()를 호출하면 광고 할당 및 갱신이 자동으로 수행됩니다.
[_viewBinder bind];
}
@end
Step 3. Implement UICollectionView
Follow these steps to create UICollectionView for the native placement carousel.
- Declare
UICollectionViewwhere you want the carousel. Configure the properties, view hierarchy, and Auto Layout ofUICollectionViewas shown below.
final class CarouselViewController: UIViewController {
// ...
// 광고를 표시할 Carousel CollectionView
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero)
collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: "CarouselCell")
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.isPagingEnabled = true
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupCarousel()
setupView()
setupLayout()
}
// ...
private func setupView() {
view.addSubview(carouselCollectionView)
}
private func setupLayout() {
// eg. auto layout constraints for carouselCollectionView
carouselCollectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
carouselCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
carouselCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
carouselCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
carouselCollectionView.heightAnchor.constraint(equalTo: carouselCollectionView.widthAnchor, multiplier: 0.8),
])
}
// ...
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
// 광고를 표시할 Carousel CollectionView
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
_adRequestCount = 5;
_loadedAdCount = 0;
_pool = [[BZVNativeAd2Pool alloc] initWithUnitId:kUnitId];
[self setupCarousel];
[self setupView];
[self setupLayout];
}
// ...
- (void)setupView {
_carouselCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero];
[_carouselCollectionView registerClass:[CarouselCell class] forCellWithReuseIdentifier:@"CarouselCell"];
_carouselCollectionView.delegate = self;
_carouselCollectionView.dataSource = self;
_carouselCollectionView.showsHorizontalScrollIndicator = NO;
_carouselCollectionView.pagingEnabled = YES;
[self.view addSubview:_carouselCollectionView];
}
- (void)setupLayout {
// eg. auto layout constraints for carouselCollectionView
self.carouselCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.carouselCollectionView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:16],
[self.carouselCollectionView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor],
[self.carouselCollectionView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor],
[self.carouselCollectionView.heightAnchor constraintEqualToAnchor:self.carouselCollectionView.widthAnchor multiplier:0.8]
]];
}
// ...
@end
- Implement the required methods of
UICollectionViewDataSourceas shown below. Set the return value ofcollectionView(_:numberOfItemsInSection:)toloadedAdCount, the actual number of allocated ads. IncollectionView(_:cellForItemAt:), dequeueCarouselCell, then callsetPool()followed bybind().
final class CarouselViewController: UIViewController {
// ...
private var loadedAdCount = 0
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero)
// ...
return collectionView
}()
// ...
}
extension CarouselViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return loadedAdCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarouselCell", for: indexPath) as? CarouselCell else {
return UICollectionViewCell()
}
// 광고 데이터들을 관리하는 NativeAd2Pool을 collectionView index에 해당하는 cell 내의 viewBinder에 설정합니다.
cell.setPool(with: pool, for: indexPath.item)
// Warining: bind를 호출하기 전에 반드시 setPool을 호출해야 합니다.
// 설정한 NativeAd2Pool의 광고 데이터를 cell에 나타냅니다. bind를 호출한 이후에는 광고 할당 및 갱신이 자동으로 수행됩니다.
cell.bind()
return cell
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@property (nonatomic, assign, readwrite) NSInteger loadedAdCount;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
_loadedAdCount = 0;
// ...
}
// ...
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _loadedAdCount;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CarouselCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CarouselCell" forIndexPath:indexPath];
// 광고 데이터들을 관리하는 NativeAd2Pool을 collectionView index에 해당하는 cell 내의 viewBinder에 설정합니다.
[cell setPool:_pool forAdKey:indexPath.item];
// Warining: bind를 호출하기 전에 반드시 setPool을 호출해야 합니다.
// 설정한 NativeAd2Pool의 광고 데이터를 cell에 나타냅니다. bind를 호출한 이후에는 광고 할당 및 갱신이 자동으로 수행됩니다.
[cell bind];
return cell;
}
@end
- Declare
UICollectionViewFlowLayout. Configure the properties ofUICollectionViewFlowLayoutas shown below and set it oncarouselCollectionView. ImplementcollectionView(_:layout:sizeForItemAt:)ofUICollectionViewDelegateFlowLayoutas shown below.
final class CarouselViewController: UIViewController {
// ...
private lazy var flowLayout: UICollectionViewFlowLayout = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = .zero
flowLayout.minimumInteritemSpacing = .zero
return flowLayout
}()
private lazy var carouselCollectionView: UICollectionView = {
// flowLayout을 carouselCollectionView에 설정하세요.
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
// ...
return collectionView
}()
// ...
}
// ...
extension CarouselViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height)
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@end
@implementation CarouselViewController
// ...
- (void)setupView {
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
flowLayout.minimumLineSpacing = 0;
flowLayout.minimumInteritemSpacing = 0;
// flowLayout을 carouselCollectionView에 설정하세요.
_carouselCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout];
// ...
}
// ...
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(collectionView.frame.size.width, collectionView.frame.size.height);
}
@end
- When
BZVNativeAd2Pool.loadAds()completes, callreloadData()to refreshcarouselCollectionView.
final class CarouselViewController: UIViewController {
// ...
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
// ...
return collectionView
}()
private lazy var pool = BZVNativeAd2Pool(unitId: unitId)
override func viewDidLoad() {
super.viewDidLoad()
// ...
setupCarousel()
}
// ...
private func setupCarousel() {
pool.loadAds(count: adRequestCount) { [weak self] adCount in
// ...
// 광고 할당이 완료되면 carouselCollectionView를 갱신합니다.
self?.carouselCollectionView.reloadData()
} errorHandler: { [weak self] error in
// ...
}
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@property (nonatomic, strong, readonly) BZVNativeAd2Pool *pool;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupCarousel];
}
// ...
- (void)setupCarousel {
__weak typeof(self) weakSelf = self;
[self.pool loadAdsWithCount:self.adRequestCount completionHandler:^(NSInteger adCount) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// ...
// 광고 할당이 완료되면 carouselCollectionView를 갱신합니다.
[strongSelf.carouselCollectionView reloadData];
}
} errorHandler:^(NSError * _Nonnull error) {
// ...
}];
}
@end
Step 4. Implement UIPageControl
Follow these steps to create a UIPageControl that indicates the carousel item's position.
- Declare
UIPageControl. Configure the properties, view hierarchy, and Auto Layout ofUIPageControlas shown below.
final class CarouselViewController: UIViewController {
// ...
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
pageControl.currentPage = .zero
pageControl.isUserInteractionEnabled = false
return pageControl
}()
// ...
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupLayout()
// ...
}
private func setupView() {
// ...
view.addSubview(pageControl)
}
private func setupLayout() {
// ...
// eg. auto layout constraints for pageControl
pageControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pageControl.topAnchor.constraint(equalTo: carouselCollectionView.bottomAnchor, constant: 16),
pageControl.centerXAnchor.constraint(equalTo: carouselCollectionView.centerXAnchor),
])
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, strong, readonly) UIPageControl *pageControl;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupView];
[self setupLayout];
// ...
}
- (void)setupView {
// ...
_pageControl = [[UIPageControl alloc] initWithFrame:CGRectZero];
_pageControl.currentPage = 0;
_pageControl.userInteractionEnabled = NO;
[self.view addSubview:self.pageControl];
}
- (void)setupLayout {
// ...
// eg. auto layout constraints for pageControl
_pageControl.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_pageControl.topAnchor constraintEqualToAnchor:_carouselCollectionView.bottomAnchor constant:16],
[_pageControl.centerXAnchor constraintEqualToAnchor:_carouselCollectionView.centerXAnchor],
]];
}
@end
- After allocating ads through
BZVNativeAd2Pool.loadAds(), setUIPageControl.numberOfPages.
final class CarouselViewController: UIViewController {
// ...
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
// ...
return pageControl
}()
// ...
override func viewDidLoad() {
super.viewDidLoad()
// ...
setupCarousel()
}
// ...
private func setupCarousel() {
pool.loadAds(count: adRequestCount) { [weak self] adCount in
// ...
self?.pageControl.numberOfPages = adCount
} errorHandler: { [weak self] error in
// ...
}
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, strong, readonly) UIPageControl *pageControl;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupCarousel];
}
// ...
- (void)setupCarousel {
__weak typeof(self) weakSelf = self;
[self.pool loadAdsWithCount:self.adRequestCount completionHandler:^(NSInteger adCount) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// ...
strongSelf.pageControl.numberOfPages = adCount;
}
} errorHandler:^(NSError * _Nonnull error) {
// ...
}];
}
@end
- Synchronize
currentPageofUIPageControlwith the carousel item index. ImplementscrollViewDidScroll(_:)ofUIScrollViewDelegateas shown below.
final class CarouselViewController: UIViewController {
// ...
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
// ...
return pageControl
}()
// ...
}
// ...
extension CarouselViewController: UICollectionViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let centerOffsetX = scrollView.contentOffset.x + (scrollView.frame.width / 2)
let pageWidth = scrollView.frame.width
// pageWidth가 0이 되는 경우 division by zero를 방지합니다.
guard pageWidth != .zero else { return }
// 현재 캐러셀 아이템의 인덱스를 pageControl의 currentPage에 지정합니다.
pageControl.currentPage = Int(centerOffsetX / pageWidth)
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, strong, readonly) UIPageControl *pageControl;
@end
@implementation CarouselViewController
// ...
#pragma mark - UICollectionViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat centerOffsetX = scrollView.contentOffset.x + (scrollView.frame.size.width / 2);
CGFloat pageWidth = scrollView.frame.size.width;
// pageWidth가 0이 되는 경우 division by zero를 방지합니다.
if (pageWidth == 0) {
return;
}
// 현재 캐러셀 아이템의 인덱스를 pageControl의 currentPage에 지정합니다.
_pageControl.currentPage = (NSInteger)centerOffsetX / (NSInteger)pageWidth;
}
@end
Run the app to see the carousel in action. Then complete the integration by following Implement required carousel features below.
Implement required carousel features
The Buzzvil SDK provides a slide page and entry point into BenefitHub, where users can find a variety of ads and filter them to suit their preferences.
BenefitHub entry slide
Add a Carousel To Feed Slide anywhere in the carousel, with no limit on the number of slides. These provide entry points between ads to BenefitHub, a placement optimized for monetization. The SDK provides the following classes.

| Code | Description |
|---|---|
BZVFeedPromotionView | The BenefitHub entry slide View object. |
BZVFeedPromotionViewBinder | Binds or unbinds BenefitHub entry slide data to the views provided to ViewBinder.bind(): Binds slide data to the views., unbind(): Unbinds slide data from the views., Builder: Builder for creating BZVFeedPromotionViewBinder. |
Follow these steps to implement a BenefitHub entry slide.
- Declare
FeedPromotionCellextendingUICollectionViewCell. Build the view as described in Implement UICollectionViewCell.
import UIKit
import BuzzvilSDK
final class FeedPromotionCell: UICollectionViewCell {
private lazy var feedPromotionView: BZVFeedPromotionView = BZVFeedPromotionView(frame: .zero)
private lazy var creativeView: UIImageView = UIImageView(frame: .zero)
private lazy var iconImageView: UIImageView = UIImageView(frame: .zero)
private lazy var titleLabel: UILabel = UILabel(frame: .zero)
private lazy var ctaView: BZVDefaultCtaView = BZVDefaultCtaView(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupLayout()
}
required init?(coder: NSCoder) {
fatalError()
}
private func setupView() {
contentView.addSubview(feedPromotionView)
feedPromotionView.addSubview(creativeView)
feedPromotionView.addSubview(iconImageView)
feedPromotionView.addSubview(titleLabel)
feedPromotionView.addSubview(ctaView)
}
private func setupLayout() {
// AutoLayout Constraints를 설정하세요.
// ...
}
}
#import "FeedPromotionCell.h"
@import BuzzvilSDK;
@interface FeedPromotionCell ()
@property (nonatomic, strong, readonly) BZVFeedPromotionView *feedPromotionView;
@property (nonatomic, strong, readonly) UIImageView *creativeView;
@property (nonatomic, strong, readonly) UIImageView *iconImageView;
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) BZVDefaultCtaView *ctaView;
@end
@implementation FeedPromotionCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupView];
[self setupLayout];
}
return self;
}
- (void)setupView {
_feedPromotionView = [[BZVFeedPromotionView alloc] initWithFrame:CGRectZero];
_creativeView = [[UIImageView alloc] initWithFrame:CGRectZero];
_iconImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_ctaView = [[BZVDefaultCtaView alloc] initWithFrame:CGRectZero];
[self.contentView addSubview:self.feedPromotionView];
[self.feedPromotionView addSubview:self.creativeView];
[self.feedPromotionView addSubview:self.iconImageView];
[self.feedPromotionView addSubview:self.titleLabel];
[self.feedPromotionView addSubview:self.ctaView];
}
- (void)setupLayout {
// AutoLayout Constraints를 설정하세요.
// ...
}
@end
- Declare
BZVFeedPromotionViewBinderinFeedPromotionCell. Set all required views, includingBZVFeedPromotionView, and create aBZVFeedPromotionViewBinderobject.
final class FeedPromotionCell: UICollectionViewCell {
private lazy var feedPromotionView: BZVFeedPromotionView = BZVFeedPromotionView(frame: .zero)
private lazy var creativeView: UIImageView = UIImageView(frame: .zero)
private lazy var iconImageView: UIImageView = UIImageView(frame: .zero)
private lazy var titleLabel: UILabel = UILabel(frame: .zero)
private lazy var ctaView: BZVDefaultCtaView = BZVDefaultCtaView(frame: .zero)
// FeedPromotionView와 하위 컴포넌트를 연결합니다.
private lazy var viewBinder = BZVFeedPromotionViewBinder
.Builder(unitId: "YOUR_NATIVE_UNIT_ID")
.feedPromotionView(feedPromotionView)
.creativeView(creativeView)
.iconImageView(iconImageView)
.titleLabel(titleLabel)
.ctaView(ctaView)
.build()
// ...
}
@interface FeedPromotionCell ()
@property (nonatomic, strong, readonly) BZVFeedPromotionView *feedPromotionView;
@property (nonatomic, strong, readonly) UIImageView *creativeView;
@property (nonatomic, strong, readonly) UIImageView *iconImageView;
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) BZVDefaultCtaView *ctaView;
@property (nonatomic, strong, readonly) BZVFeedPromotionViewBinder *viewBinder;
@end
@implementation FeedPromotionCell
// ...
- (void)setupView {
// ...
// NativeAd2View와 하위 컴포넌트를 연결합니다.
_viewBinder = [BZVFeedPromotionViewBinder viewBinderWithBlock:^(BZVFeedPromotionViewBinderBuilder * _Nonnull builder) {
builder.unitId = @"YOUR_NATIVE_UNIT_ID";
builder.feedPromotionView = self.feedPromotionView;
builder.creativeView = self.creativeView;
builder.iconImageView = self.iconImageView;
builder.titleLabel = self.titleLabel;
builder.ctaView = self.ctaView;
}];
}
// ...
@end
Implement bind() in FeedPromotionCell to configure cell data for the UICollectionView index. Call unbind() in prepareForReuse() to clear old ad data when reusing a cell.
final class FeedPromotionCell: UICollectionViewCell {
// ...
private lazy var viewBinder = BZVFeedPromotionViewBinder
.Builder(unitId: "YOUR_NATIVE_UNIT_ID")
.feedPromotionView(feedPromotionView)
.creativeView(creativeView)
.iconImageView(iconImageView)
.titleLabel(titleLabel)
.ctaView(ctaView)
.build()
// ...
override func prepareForReuse() {
super.prepareForReuse()
// prepareForReuse 내에서 unbind를 반드시 호출하여 cell을 재사용할 때 문제가 발생하지 않게 합니다.
viewBinder.unbind()
}
// collectionView의 collectionView(_:cellForItemAt:) 시점에 호출합니다.
func bind() {
// BZVFeedPromotionViewBinder의 bind()를 호출하면 베네핏허브 진입 슬라이드 데이터가 자동으로 제공됩니다.
viewBinder.bind()
}
}
@interface FeedPromotionCell : UICollectionViewCell
- (void)bind;
@end
@interface FeedPromotionCell ()
// ...
@property (nonatomic, strong, readonly) BZVFeedPromotionViewBinder *viewBinder;
@end
@implementation FeedPromotionCell
// ...
- (void)prepareForReuse {
[super prepareForReuse];
// prepareForReuse 내에서 unbind를 반드시 호출하여 cell을 재사용할 때 문제가 발생하지 않게 합니다.
[_viewBinder unbind];
}
// collectionView의 collectionView(_:cellForItemAt:) 시점에 호출합니다.
- (void)bind {
// BZVFeedPromotionViewBinder의 bind()를 호출하면 베네핏허브 진입 슬라이드 데이터가 자동으로 제공됩니다.
[_viewBinder bind];
}
@end
- Register
FeedPromotionCellwith the carouselUICollectionViewthat displays ads.
final class CarouselViewController: UIViewController {
// ...
// 광고를 표시할 Carousel CollectionView
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero)
collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: "CarouselCell")
collectionView.register(FeedPromotionCell.self, forCellWithReuseIdentifier: "FeedPromotionCell")
// ...
return collectionView
}()
// ...
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// 광고를 표시할 Carousel CollectionView
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@end
@implementation CarouselViewController
// ...
- (void)setupView {
_carouselCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero];
[_carouselCollectionView registerClass:[CarouselCell class] forCellWithReuseIdentifier:@"CarouselCell"];
[_carouselCollectionView registerClass:[FeedPromotionCell class] forCellWithReuseIdentifier:@"FeedPromotionCell"];
// ...
}
// ...
@end
- Update
loadedAdCountwhenBZVNativeAd2Pool.loadAds()succeeds, as described in Allocate ads. The following example updatesloadedAdCountto the number of allocated ads plus one BenefitHub entry slide. It also displays one BenefitHub entry slide ifBZVNativeAd2Pool.loadAds()fails to allocate ads.
final class CarouselViewController: UIViewController {
// ...
private let adRequestCount = 5
private let feedPromotionSlideCount = 1
private var loadedAdCount = 0
private lazy var pool = BZVNativeAd2Pool(unitId: unitId)
// ...
private func setupCarousel() {
pool.loadAds(count: adRequestCount) { [weak self] adCount in
// ...
// 실제로 할당받은 광고의 개수(adCount)에 베네핏허브 진입 슬라이드 개수(feedPromotionSlideCount)를 더해서 cell 개수(loadedAdCount)를 설정합니다.
self?.loadedAdCount = adCount + (self?.feedPromotionSlideCount ?? 1)
self?.carouselCollectionView.reloadData()
} errorHandler: { [weak self] error in
// ...
// 광고 할당이 실패했을 때, 베네핏허브 진입 슬라이드 1개가 보이도록 구성합니다.
self?.loadedAdCount = self?.feedPromotionSlideCount ?? 1
self?.carouselCollectionView.reloadData()
}
}
}
@interface CarouselViewController ()
// ...
@property (nonatomic, assign, readonly) NSInteger adRequestCount;
@property (nonatomic, assign, readonly) NSInteger feedPromotionSlideCount;
@property (nonatomic, assign, readwrite) NSInteger loadedAdCount;
@property (nonatomic, strong, readonly) BZVNativeAd2Pool *pool;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
_adRequestCount = 5;
_feedPromotionSlideCount = 1;
_loadedAdCount = 0;
_pool = [[BZVNativeAd2Pool alloc] initWithUnitId:FEED_UNIT_ID];
// ...
}
- (void)setupCarousel {
__weak typeof(self) weakSelf = self;
[self.pool loadAdsWithCount:self.adRequestCount completionHandler:^(NSInteger adCount) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// ...
// 실제로 할당받은 광고의 개수(adCount)에 베네핏허브 진입 슬라이드 개수(feedPromotionSlideCount)를 더해서 cell 개수(loadedAdCount)를 설정합니다.
strongSelf.loadedAdCount = adCount + strongSelf.feedPromotionSlideCount;
[strongSelf.carouselCollectionView reloadData];
}
} errorHandler:^(NSError * _Nonnull error) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// ...
// 광고 할당이 실패했을 때, 베네핏허브 진입 슬라이드 1개가 보이도록 구성합니다.
strongSelf.loadedAdCount = strongSelf.feedPromotionSlideCount;
[strongSelf.carouselCollectionView reloadData];
}
}];
}
@end
- In
collectionView(_:cellForItemAt:)ofUICollectionViewDataSource, returnFeedPromotionCellwhen appropriate. The following example returnsFeedPromotionCellfor the last index.
final class CarouselViewController: UIViewController {
// ...
private var loadedAdCount = 0
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero)
// ...
return collectionView
}()
// ...
}
extension CarouselViewController: UICollectionViewDataSource {
// ...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if (indexPath.item % loadedAdCount) == loadedAdCount - 1 {
// last index인 경우 FeedPromotionCell을 반환합니다.
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FeedPromotionCell", for: indexPath) as? FeedPromotionCell else {
return UICollectionViewCell()
}
cell.bind()
return cell
} else {
// last index가 아닌 경우 CarouselCell을 반환합니다.
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarouselCell", for: indexPath) as? CarouselCell else {
return UICollectionViewCell()
}
cell.setPool(with: pool, for: indexPath.item)
cell.bind()
return cell
}
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@property (nonatomic, assign, readwrite) NSInteger loadedAdCount;
@end
@implementation CarouselViewController
// ...
#pragma mark - UICollectionViewDataSource
// ...
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
if ((indexPath.item % _loadedAdCount) == _loadedAdCount - 1) {
// last index인 경우 FeedPromotionCell을 반환합니다.
FeedPromotionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"FeedPromotionCell" forIndexPath:indexPath];
[cell bind];
return cell;
} else {
// last index가 아닌 경우 CarouselCell을 반환합니다.
CarouselCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CarouselCell" forIndexPath:indexPath];
[cell setPool:_pool forAdKey:indexPath.item];
[cell bind];
return cell;
}
}
@end
BenefitHub entry point
Add a Carousel To Feed Link below the carousel so users can open BenefitHub.

Follow these steps to implement the BenefitHub entry point.
-
Declare
CarouselFeedEntryViewextendingBZVFeedEntryView.
import UIKit
import BuzzvilSDK
final class CarouselFeedEntryView: BZVFeedEntryView {
private lazy var button: UIButton = {
let button = UIButton(type: .system)
button.setTitle("포인트 더 받으러 가기", for: .normal)
button.tintColor = .systemBlue
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupLayout()
}
required init?(coder: NSCoder) {
fatalError()
}
private func setupView() {
addSubview(button)
clickableViews = [button]
}
private func setupLayout() {
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: topAnchor),
button.leadingAnchor.constraint(equalTo: leadingAnchor),
button.trailingAnchor.constraint(equalTo: trailingAnchor),
button.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}
@import UIKit;
@import BuzzvilSDK;
@interface CarouselFeedEntryView: BZVFeedEntryView
@end
@interface CarouselFeedEntryView ()
@property (nonatomic, strong, readonly) UIButton *button;
@end
@implementation CustomFeedEntryView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupView];
[self setupLayout];
}
return self;
}
- (void)setupView {
_button = [[UIButton alloc] initWithFrame:CGRectZero];
[_button setTitle:@"포인트 더 받으러 가기" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor systemBlueColor] forState:UIControlStateNormal];
[self addSubview:_button];
self.clickableViews = @[self.button];
}
- (void)setupLayout {
_button.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_button.topAnchor constraintEqualToAnchor:self.topAnchor],
[_button.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_button.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_button.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
]];
}
@end
- Add
CarouselFeedEntryViewbelowCarouselCollectionView. Configure the view hierarchy and Auto Layout forCarouselFeedEntryViewappropriately.
final class CarouselViewController: UIViewController {
// ...
private lazy var feedEntryView: CarouselFeedEntryView = {
let view = CarouselFeedEntryView(frame: .zero)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupLayout()
// ...
}
private func setupView() {
// ...
view.addSubview(feedEntryView)
}
private func setupLayout() {
// AutoLayout Constraints를 설정하세요.
// ...
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, strong, readonly) CarouselFeedEntryView *feedEntryView;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupView];
[self setupLayout];
// ...
}
- (void)setupView {
// ...
_feedEntryView = [[CarouselFeedEntryView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.feedEntryView];
}
- (void)setupLayout {
// AutoLayout Constraints를 설정하세요.
// ...
}
@end
-
Control the BenefitHub entry point's visibility according to the ad allocation result.
- Show the carousel and BenefitHub entry point when there are ads to display.
- Hide the carousel and BenefitHub entry point while ads have not yet loaded, if an error occurs, or if there are no ads.
The following example changes isHidden on CarouselCollectionView and CarouselFeedEntryView according to allocation success in CarouselViewController.
final class CarouselViewController: UIViewController {
// ...
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
// ...
return collectionView
}()
private lazy var feedEntryView: CarouselFeedEntryView = {
let view = CarouselFeedEntryView(frame: .zero)
return view
}()
private lazy var pool = BZVNativeAd2Pool(unitId: unitId)
override func viewDidLoad() {
super.viewDidLoad()
// ...
setupCarousel()
}
// ...
private func setupCarousel() {
pool.loadAds(count: adRequestCount) { [weak self] adCount in
// ...
} errorHandler: { [weak self] error in
self?.carouselCollectionView.isHidden = true
self?.feedEntryView.isHidden = true
}
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@property (nonatomic, strong, readonly) CarouselFeedEntryView *feedEntryView;
@property (nonatomic, strong, readonly) BZVNativeAd2Pool *pool;
@end
@implementation CarouselViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupCarousel];
}
// ...
- (void)setupCarousel {
__weak typeof(self) weakSelf = self;
[self.pool loadAdsWithCount:self.adRequestCount completionHandler:^(NSInteger adCount) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// ...
}
} errorHandler:^(NSError * _Nonnull error) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.carouselCollectionView.hidden = YES;
strongSelf.feedEntryView.hidden = YES;
}
}];
}
@end
-
Keep the BenefitHub entry point position fixed.
Different lengths of ad description text (
textDescription) can produce different line counts, making the entry point shift between slides. Set a fixednumberOfLinesvalue on the description view to keep it in a consistent position.The following example sets the description label's
numberOfLinesto 2.
final class CarouselCell: UICollectionViewCell {
// ...
private lazy var descriptionLabel: UILabel = {
let label = UILabel(frame: .zero)
label.numberOfLines = 2
return label
}()
// ...
}
@interface CarouselCell ()
// ...
@property (nonatomic, strong, readonly) UILabel *descriptionLabel;
@end
@implementation CarouselCell
- (void)setupView {
// ...
_descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_descriptionLabel.numberOfLines = 2;
// ...
}
// ...
@end
Run the app to see the BenefitHub entry slide on the last carousel page and the entry point below the carousel. Then see Implement optional carousel features below to provide a user-friendly UI and UX.
Implement optional carousel features
This section presents some of the ways you can add features to a carousel. Adapt the code to your needs.
Implement infinite looping
You can implement an infinite loop that lets users scroll continuously through a finite set of carousel items. There are many approaches; this guide uses a simple method in which collectionView(_:numberOfItemsInSection:) returns a large value.
Follow these steps to implement an infinite loop.
- Modify
collectionView(_:numberOfItemsInSection:)ofUICollectionViewDataSourceto make the carousel appear to have infinite data. You must also synchronizeUIPageControl.numberOfPages.
final class CarouselViewController: UIViewController {
// ...
private var loadedAdCount = 0
private var infiniteItemCount: Int {
return loadedAdCount * 1000
}
// ...
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
// ...
return pageControl
}()
override func viewDidLoad() {
super.viewDidLoad()
// ...
setupCarousel()
}
// ...
private func setupCarousel() {
pool.loadAds(count: adRequestCount) { [weak self] adCount in
// ...
// 고유한 cell 개수를 저장합니다.
self?.loadedAdCount = adCount
// 총 cell 개수를 pageControl 개수로 설정합니다.
self?.pageControl.numberOfPages = self?.infiniteItemCount ?? adCount * 1000
} errorHandler: { [weak self] error in
// ...
}
}
}
extension CarouselViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return infiniteItemCount
}
// ...
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, assign, readwrite) NSInteger loadedAdCount;
@property (nonatomic, strong, readonly) UIPageControl *pageControl;
// ...
@end
@implementation CarouselViewController
// ...
- (NSInteger)infiniteItemCount {
return _loadedAdCount * 1000;
}
// ...
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupCarousel];
}
// ...
- (void)setupCarousel {
__weak typeof(self) weakSelf = self;
[self.pool loadAdsWithCount:self.adRequestCount completionHandler:^(NSInteger adCount) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// ...
// 고유한 cell 개수를 저장합니다.
strongSelf.loadedAdCount = adCount;
// 총 cell 개수를 pageControl 개수로 설정합니다.
strongSelf.pageControl.numberOfPages = [strongSelf infiniteItemCount];
}
} errorHandler:^(NSError * _Nonnull error) {
// ...
}];
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [self infiniteItemCount];
}
// ...
@end
- Modify
collectionView(_:cellForItemAt:)ofUICollectionViewDataSourceas shown below. Use modulo with the number of unique cells to calculate the index.
extension CarouselViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarouselCell", for: indexPath) as? CarouselCell else {
return UICollectionViewCell()
}
// 할당된 고유 광고 개수로 모듈러 연산하여 index를 적용합니다.
cell.setPool(with: pool, for: indexPath.item % loadedAdCount)
cell.bind()
return cell
}
}
@implementation CarouselViewController
// ...
#pragma mark - UICollectionViewDataSource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CarouselCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CarouselCell" forIndexPath:indexPath];
// 할당된 고유 광고 개수로 모듈러 연산하여 index를 적용합니다.
[cell setPool:_pool forAdKey:indexPath.item % self.loadedAdCount];
[cell bind];
return cell;
}
// ...
@end
- After allocating ads through
BZVNativeAd2Pool.loadAds(), move theUICollectionViewindex to the middle.
final class CarouselViewController: UIViewController {
// ...
private var loadedAdCount = 0
private var infiniteItemCount: Int {
return loadedAdCount * 1000
}
// ...
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
// ...
return collectionView
}()
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
// ...
return pageControl
}()
override func viewDidLoad() {
super.viewDidLoad()
// ...
setupCarousel()
}
// ...
private func setupCarousel() {
pool.loadAds(count: adRequestCount) { [weak self] adCount in
// ...
self?.loadedAdCount = adCount
self?.pageControl.numberOfPages = infiniteItemCount
self?.carouselCollectionView.reloadData()
self?.moveCarouselToMiddle()
} errorHandler: { [weak self] error in
// ...
}
}
private func moveCarouselToMiddle() {
let middleIndex = ((loadedAdCount * 1000) / 2) % loadedAdCount
let indexPath = IndexPath(item: middleIndex, section: .zero)
carouselCollectionView.scrollToItem(at: indexPath, at: [.centeredVertically, .centeredHorizontally], animated: false)
pageControl.currentPage = indexPath.item
}
}
@interface CarouselViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
// ...
@property (nonatomic, assign, readwrite) NSInteger loadedAdCount;
@property (nonatomic, strong, readonly) UICollectionView *carouselCollectionView;
@property (nonatomic, strong, readonly) UIPageControl *pageControl;
// ...
@end
@implementation CarouselViewController
- (NSInteger)infiniteItemCount {
return _loadedAdCount * 1000;
}
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupCarousel];
}
// ...
- (void)setupCarousel {
__weak typeof(self) weakSelf = self;
[self.pool loadAdsWithCount:self.adRequestCount completionHandler:^(NSInteger adCount) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// ...
strongSelf.loadedAdCount = adCount;
strongSelf.pageControl.numberOfPages = [strongSelf infiniteItemCount];
[strongSelf.carouselCollectionView reloadData];
[strongSelf moveCarouselToMiddle];
}
} errorHandler:^(NSError * _Nonnull error) {
// ...
}];
}
- (void)moveCarouselToMiddle {
NSInteger middleIndex = ((self.loadedAdCount * 1000) / 2) % _loadedAdCount;
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:middleIndex inSection:0];
[self.carouselCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredVertically | UICollectionViewScrollPositionCenteredHorizontally animated:NO];
_pageControl.currentPage = indexPath.item;
}
// ...
@end
Implement a loading screen
Use BZVNativeAd2ViewBinder.subscribeEvents() to show a loading screen during refreshes after ad participation, visually indicating network request delays.
final class CarouselCell: UICollectionViewCell {
// ...
private lazy var activityIndicatorView: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(frame: .zero)
indicator.hidesWhenStopped = true
return indicator
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupLayout()
}
private func setupView() {
// ...
contentView.addSubview(activityIndicatorView)
}
private func setupLayout() {
// ...
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
func setupLoading() {
viewBinder.subscribeEvents(onRequest: { [weak self] in
self?.activityIndicatorView.startAnimating()
self?.nativeAd2View.alpha = 0.5
}, onNext: { [weak self] _ in
self?.activityIndicatorView.stopAnimating()
self?.nativeAd2View.alpha = 1
}, onError: { [weak self] in
self?.activityIndicatorView.stopAnimating()
self?.nativeAd2View.alpha = 1
print("error: \($0)")
}, onCompleted: { [weak self] in
self?.activityIndicatorView.stopAnimating()
self?.nativeAd2View.alpha = 1
print("completed")
})
}
}
extension CarouselViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// ...
cell.setupLoading()
cell.setPool(with: pool, for: indexPath.item)
cell.bind()
return cell
}
}
@interface CarouselCell : UICollectionViewCell
// ...
- (void)setupLoading
@end
@interface CarouselCell ()
// ...
@property (nonatomic, strong, readonly) UIActivityIndicatorView *activityIndicatorView;
@end
@implementation CarouselCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupView];
[self setupLayout];
}
return self;
}
- (void)setupView {
// ...
_activityIndicatorView = [[UIActivityIndicatorView alloc] initWithFrame:CGRectZero];
_activityIndicatorView.hidesWhenStopped = YES;
[self.contentView addSubview:_activityIndicatorView];
}
- (void)setupLayout {
// ...
_activityIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_activityIndicatorView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_activityIndicatorView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
]];
}
- (void)setupLoading {
__weak typeof(self) weakSelf = self;
[_viewBinder subscribeEventsOnRequest:^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView startAnimating];
strongSelf.nativeAd2View.alpha = 0.5;
}
} onNext:^(BZVNativeAd2 * _Nonnull ad) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView stopAnimating];
strongSelf.nativeAd2View.alpha = 1;
}
} onError:^(NSError * _Nonnull error) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView stopAnimating];
strongSelf.nativeAd2View.alpha = 1;
NSLog(@"error: %@", error);
}
} onCompleted:^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.activityIndicatorView stopAnimating];
strongSelf.nativeAd2View.alpha = 1;
NSLog(@"completed");
}
}];
}
@end
@implementation CarouselViewController
// ...
#pragma mark - UICollectionViewDataSource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// ...
[cell setupLoading];
[cell setPool:_pool forIndex:indexPath.item];
[cell bind];
return cell;
}
@end
Register an ad event listener
Record when carousel ad events occur, such as impressions and clicks, to understand user behavior.
final class CarouselCell: UICollectionViewCell {
// ...
func setupEventListeners() {
viewBinder.subscribeAdEvents(onImpressed: { [weak self] in
print("impressed: \($0.title)")
}, onClicked: { [weak self] in
print("clicked: \($0.title)")
}, onRewardRequested: { [weak self] in
print("requested reward: \($0.title)")
}, onRewarded: { [weak self] in
print("received reward result: \($0.title), \($1)")
}, onParticipated: { [weak self] in
print("participated: \($0.title)")
})
}
}
extension CarouselViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// ...
cell.setupEventListeners()
cell.setPool(with: pool, for: indexPath.item)
cell.bind()
return cell
}
}
@interface CarouselCell : UICollectionViewCell
// ...
- (void)setupEventListeners;
@end
@implementation CarouselCell
// ...
- (void)setupEventListeners {
__weak typeof(self) weakSelf = self;
[_viewBinder subscribeAdEventsOnImpressed:^(BZVNativeAd2 * _Nonnull ad) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"impressed: %@", ad.title);
}
} onClicked:^(BZVNativeAd2 * _Nonnull ad) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"clicked: %@", ad.title);
}
} onRewardRequested:^(BZVNativeAd2 * _Nonnull ad) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"requested reward: %@", ad.title);
}
} onRewarded:^(BZVNativeAd2 * _Nonnull ad, BZVRewardResult result) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"received reward result: %@", ad.title);
}
} onParticipated:^(BZVNativeAd2 * _Nonnull ad) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"participated: %@", ad.title);
}
}];
}
@end
@implementation CarouselViewController
// ...
#pragma mark - UICollectionViewDataSource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// ...
[cell setupEventListeners];
[cell setPool:_pool forIndex:indexPath.item];
[cell bind];
return cell;
}
@end
Partially show neighboring ads
Follow these steps to show the previous and next carousel items partially.
- Change the Auto Layout of
CarouselCellas shown below.
final class CarouselCell: UICollectionViewCell {
private lazy var nativeAd2View = BZVNativeAd2View(frame: .zero)
// ...
override init(frame: CGRect) {
super.init(frame: frame)
// ...
setupLayout()
// ...
}
private func setupLayout() {
// CarouselCell과 동일한 크기로 설정합니다.
nativeAd2View.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nativeAd2View.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
nativeAd2View.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
nativeAd2View.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
nativeAd2View.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
])
// ...
}
// ...
}
@interface CarouselCell ()
@property (nonatomic, strong, readonly) BZVNativeAd2View *nativeAd2View;
// ...
@end
@implementation CarouselCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// ...
[self setupLayout];
// ...
}
return self;
}
- (void)setupLayout {
// CarouselCell과 동일한 크기로 설정합니다.
_nativeAd2View.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_nativeAd2View.topAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.topAnchor],
[_nativeAd2View.leadingAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.leadingAnchor],
[_nativeAd2View.trailingAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.trailingAnchor],
[_nativeAd2View.bottomAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.bottomAnchor],
]];
// ...
}
// ...
@end
- Set
flowLayoutandcarouselCollectionViewofCarouselViewControlleras shown below. SetisPagingEnabledofUICollectionViewtofalsewhen configuring the carousel to show adjacent items.
final class CarouselViewController: UIViewController {
private let spacing: CGFloat = 8
// ...
private lazy var flowLayout: UICollectionViewFlowLayout = {
let flowLayout = UICollectionViewFlowLayout()
// ...
// flowLayout의 minimumLineSpacing을 spacing 값으로 설정합니다.
flowLayout.minimumLineSpacing = spacing
return flowLayout
}()
private lazy var carouselCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
// ...
// isPagingEnabled를 false로 설정합니다.
collectionView.isPagingEnabled = false
// 아래 속성들을 설정합니다.
collectionView.decelerationRate = .fast
collectionView.contentInset = UIEdgeInsets(top: .zero, left: 2 * spacing, bottom: .zero, right: 2 * spacing)
return collectionView
}()
// ...
}
@implementation CarouselViewController
- (NSInteger)spacing {
return 8;
}
- (void)viewDidLoad {
[super viewDidLoad];
// ...
[self setupView];
// ...
}
- (void)setupView {
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
// flowLayout의 minimumLineSpacing을 spacing 값으로 설정합니다.
flowLayout.minimumLineSpacing = [self spacing];
// ...
_carouselCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout];
// ...
// isPagingEnabled를 false로 설정합니다.
_carouselCollectionView.pagingEnabled = NO;
// 아래 속성들을 설정합니다.
_carouselCollectionView.decelerationRate = UIScrollViewDecelerationRateFast;
_carouselCollectionView.contentInset = UIEdgeInsetsMake(0, 2 * [self spacing], 0, 2 * [self spacing]);
// ...
}
// ...
@end
- Because this does not use the
isPagingEnabledvalue ofUICollectionView, implement paging yourself. ImplementscrollViewWillEndDragging(_:withVelocity:targetContentOffset:)ofUICollectionViewDelegateas shown below and move the code that updates thepageControlindex here as well.
final class CarouselViewController: UIViewController {
private let spacing = 8
// ...
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
// ...
return pageControl
}()
// ...
}
// 위에서 구현했던 scrollViewDidScroll(_:)를 제거합니다.
//
//extension CarouselViewController: UICollectionViewDelegate {
// func scrollViewDidScroll(_ scrollView: UIScrollView) {
// let centerOffsetX = scrollView.contentOffset.x + (scrollView.frame.width / 2)
// let pageWidth = scrollView.frame.width
// guard pageWidth != .zero else { return }
// pageControl.currentPage = Int(centerOffsetX / pageWidth)
// }
//}
// 캐러셀 아이템이 화면에서 일정 부분 넘어가면 다음 아이템으로 이동하도록 구현합니다.
extension CarouselViewController: UICollectionViewDelegate {
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let cellWidth = scrollView.frame.width - (4 * spacing)
let cellWidthWithSpace = cellWidth + spacing
let estimatedIndex = scrollView.contentOffset.x / cellWidthWithSpace
let index: Int
if velocity.x > 0 {
index = Int(ceil(estimatedIndex))
} else if velocity.x < 0 {
index = Int(floor(estimatedIndex))
} else {
index = Int(round(estimatedIndex))
}
pageControl.currentPage = index
targetContentOffset.pointee = CGPoint(x: CGFloat(index) * cellWidthWithSpace - (2 * spacing), y: .zero)
}
}
@interface CarouselViewController ()
// ...
@property (nonatomic, strong, readonly) UIPageControl *pageControl;
@end
@implementation CarouselViewController
- (NSInteger)spacing {
return 8;
}
// ...
// 위에서 구현했던 scrollViewDidScroll(_:)를 제거합니다.
//
//#pragma mark - UICollectionViewDelegate
//
//- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// CGFloat centerOffsetX = scrollView.contentOffset.x + (scrollView.frame.size.width / 2);
// CGFloat pageWidth = scrollView.frame.size.width;
//
// if (pageWidth == 0) {
// return;
// }
// _pageControl.currentPage = (NSInteger)centerOffsetX / (NSInteger)pageWidth;
//}
#pragma mark - UICollectionViewDelegate
// 캐러셀 아이템이 화면에서 일정 부분 넘어가면 다음 아이템으로 이동하도록 구현합니다.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGFloat cellWidth = scrollView.frame.size.width - (4 * [self spacing]);
CGFloat cellWidthWithSpace = cellWidth + [self spacing];
CGFloat estimatedIndex = scrollView.contentOffset.x / cellWidthWithSpace;
NSInteger index;
if (velocity.x > 0) {
index = (NSInteger)ceil(estimatedIndex);
} else if (velocity.x < 0) {
index = (NSInteger)floor(estimatedIndex);
} else {
index = (NSInteger)round(estimatedIndex);
}
_pageControl.currentPage = index;
targetContentOffset->x = index * cellWidthWithSpace - (2 * [self spacing]);
targetContentOffset->y = 0;
}
@end
- Modify
collectionView(_:layout:sizeForItemAt:)ofUICollectionViewDelegateFlowLayoutas shown below.
final class CarouselViewController: UIViewController {
private let spacing = 8
// ...
}
// ...
extension CarouselViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.width - (4 * spacing), height: collectionView.frame.size.height)
}
}
@implementation CarouselViewController
- (NSInteger)spacing {
return 8;
}
// ...
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(collectionView.frame.size.width - (4 * [self spacing]), collectionView.frame.size.height);
}
@end
Adjust spacing between ads
To adjust spacing between carousel ad items, change the spacing value configured in Partially show adjacent ad items.
final class CarouselViewController: UIViewController {
// eg. spacing 값을 12로 설정합니다.
private let spacing = 12
// ...
}
@implementation CarouselViewController
// eg. spacing 값을 12로 설정합니다.
- (NSInteger)spacing {
return 12;
}
// ...
@end