네이티브(Native)
네이티브를 추가하고 설정하고 SDK 인터페이스를 사용해 부가 기능을 구현하는 방법을 알아보세요.
주요 특징
Buzzvil SDK가 제공하는 네이티브는 앱 화면과 자연스럽게 임베드되는 앱 맞춤형 광고를 게재할 수 있는 인벤토리입니다. 또한 유저가 광고에 참여한 후 자동으로 다음 광고를 노출하여, 하나의 네이티브 지면에서 끊임없이 광고를 송출하여 앱 수익화를 가속할 수 있도록 진화된 기능입니다. 유저가 더 많은 광고에 참여할 수 있도록 유도하는 다이나믹한 기능들도 제공합니다.
연동
앱에 네이티브를 연동하는 방법을 확인해 보세요.
1 단계. 준비하기
- 시작하기 적용 완료
- 네이티브 지면에 사용할 Unit ID (이하
YOUR_NATIVE_UNIT_ID
)
2 단계. 광고 레이아웃 구성하기
네이티브 광고 레이아웃을 구성하는 요소와 UI에 대해서는 아래의 그림과 표를 참고하세요.
구성 요소 | 설명 |
---|---|
광고 제목 (필수) | 광고의 제목입니다. 최대 10자까지 권장하며, 필요에 따라 글자 수에 상관 없이 일정 부분은 생략 부호도 대체할 수 있습니다. |
광고 소재 (필수) |
|
광고 설명 (필수) | 광고에 대한 상세 설명입니다. 최대 40자까지 권장하며, 필요에 따라 글자 수에 상관 없이 일정 부분은 생략 부호로 대체할 수 있습니다. |
광고주 아이콘 (필수) |
|
CTA 버튼 (필수) |
|
베네핏허브 진입점(BZVNativeToFeedView ) (필수) | 유저가 클릭하면 바로 베네핏허브로 이동하는 UI(BZVNativeToFeedView )입니다. NativeAd2View 레이아웃 아래에 추가하세요. |
네이티브 광고 레이아웃은 뷰 컨트롤러 또는 커스텀 뷰에 아래 구조에 맞게 구성해야 합니다. 네이티브 광고 지면을 구현하려면 다음의 절차를 따르세요.
다음은 네이티브 광고 레이아웃 규격에 따라 뷰 컨트롤러를 구성하는 예시입니다.
✏️ 참고
BZVNativeToFeedView
은 자유롭게 구성할 수 있습니다. 그림과 같이 베네핏허브 이동 안내 텍스트와 화살표 아이콘을 적용하는 것을 추천합니다.- 베네핏허브 이동 안내 텍스트에 베네핏허브에 진입만 해도 받을 수 있는 포인트를 표시할 수도 있습니다. 자세한 내용은 베이스 리워드 금액 표시하기 토픽을 참고하세요.
- Swift
- Objective-C
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
✏️ 참고
뷰 컨트롤러나 커스텀 뷰 이외의 다른 방식으로 네이티브 지면을 구현하려면 버즈빌 담당자(help@buzzvilcom)에게 문의하세요.
3 단계. 광고 보여주기
광고 레이아웃에 광고를 보여주기 위해 예시 코드를 참고하여 다음의 절차를 따르세요.
- 생성된
BZVNativeAd2ViewBinder
객체의bind()
를 호출하면 자동으로 광고를 할당받고 레이아웃에 표시합니다.- 광고 할당에 실패하면 광고가 표시되지 않습니다. 실패 시 동작을 정의하려면 2번 절차를 참고하세요.
- 사용자가 광고 참여 완료시 자동으로 다음 광고로 갱신됩니다.
- (Optional) 광고 요청 상태에 따른 UI 등 원하는 동작을 구현하려면
subscribeEvents()
함수의 인자로onRequest
,onNext
,onError
,onCompleted
closure를 작성하여 필요한 시점에 원하는 동작을 수행하세요.bind()
함수를 호출하기 전에subscribeEvents()
를 호출하세요.- 한번 closure를 설정해두면, 이후 자동 갱신에서도 동일하게 사용됩니다.
✏️ 참고
- 참여 완료 상태가 존재하지 않는 논리워드 콘텐츠, 이미 참여 완료된 광고는 클릭 후 랜딩 페이지에서 네이티브 지면으로 돌아올 때 다음 광고로 갱신됩니다.
- 유저가 광고에 참여 중인 상태(CTA View가 '참여 확인 중'인 경우)에는 다음 광고로 갱신되지 않습니다.
- 자동 광고 갱신을 비활성화하려면 버즈빌 담당자(help@buzzvil.com)에게 문의하세요.
⚠️ 주의
모든 closure 내에서 네이티브 지면이 구성된 클래스에 접근한다면, retain cycle을 방지하기 위해 반드시weak self
를 사용해주세요.
- Swift
- Objective-C
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
✏️ 참고
subscribeEvents()
함수의onError
closure가 호출되고 광고가 노출되지 않습니다. 광고 미할당 시 발생하는NSError
오류 코드에 대한 자세한 내용은 오류 코드가 나타납니다 토픽을 참고하세요.- 광고 갱신 시 할당 받을 수 있는 광고가 없을 때는
subscribeEvents()
함수의onCompleted
가 호출되고 이전 광고가 유지됩니다.
베이스 리워드 금액 표시하기
BZVNativeToFeedView
에 유저가 베네핏허브에 진입하기만 해도 받을 수 있는 베이스 리워드(BaseReward
) 금액 표시해 유저의 클릭율을 높일 수 있습니다.
다음은 BZVNativeToFeedView
에 기본 적립 포인트를 표시하는 예시입니다.
- Swift
- Objective-C
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 = @"더 많은 참여 기회 보기";
}
}
}];
❗️ 주의
유저에게 지급하는 포인트 금액의 정확하게 표시하기 위해UIViewController
의viewDidLoad()
시점에 기본 적립 포인트를 포함한 유도 문구를 업데이트해야 합니다.
4 단계. 기본 연동 테스트하기
모든 기본 연동 단계를 완료한 후 광고가 정상적으로 할당되고 표시되는지 확인하세요. 그리고 NativeToFeedLayout
(네이티브 광고 지면에 베네핏허브로 이동하는 레이아웃)이 제대로 동작하여 클릭하면 베네핏허브로 이동하는지 확인하세요.
추가 구현
네이티브에 추가 기능을 구현하는 방법을 안내합니다.
네이티브 오버레이 사용하기
✅ 중요
이 기능은 Buzzvil SDK 5.9.x 부터 기본적으로 제공됩니다.
✏️ 참고
네이티브 오버레이의 UI를 커스터마이징하려면 네이티브 오버레이 UI 를 참고하세요.
네이티브 오버레이는 별도의 연동이나 구현할 필요 없는 기능으로, 유저가 네이티브 광고에 참여한 후 앱으로 되돌아오면 참여 완료된 광고 위에 베네핏허브로의 진입을 유도하는 오버레이 UI를 제공합니다. 오버레이 UI는 텍스트와 아이콘으로 구성되어 있으며, 네이티브 전체 영역(NativeAdView
)을 덮어 유저의 클릭을 유도합니다. 유저가 닫기 버튼을 클릭하거나 오버레이를 클릭한 후 다시 돌아오면 오버레이는 자동으로 사라집니다.
네이티브 지면 구현 크기에 따른 오버레이 UI 를 제공하며 네이티브 오버레이에서는 베네핏허브 진입 비율을 높이기 위해 유저의 과거 참여 이력을 기반으로 선호하는 광고나 콘텐츠 광고 상품의 UI를 다양하게 노출됩니다. 각 UI 종류별 랜딩 페이지는 아래와 같습니다.
- 데일리 리워드: 베네핏허브 데일리 리워드 바텀시트
- 베네핏허브 안에서 데일리 리워드를 활성화하지 않으면 노출되지 않습니다.
- 데일리 리워드에 대해서는 여기를 참고하세요.
- 퀴즈적립: 베네핏허브 콘텐츠적립 필터
- 운세적립: 베네핏허브 콘텐츠적립 필터
- 포춘쿠키: 베네핏허브 콘텐츠적립 필터
- 베이스 리워드: 베네핏허브
- 베네핏허브 안에서 베이스 리워드를 활성화하지 않으면 노출되지 않습니다.
- 적립가능한 리워드 총액: 베네핏허브
- 기본 UI: 베네핏허브
- 서버 통신 에러 발생 시 나타납니다.
동영상 광고 리스너 등록하기
동영상 광고에서 발생하는 콜백 이벤트를 수신할 수 있습니다.
다음은 BZVNativeAd2View
에 동영상 광고 이벤트 리스너를 등록하는 예시입니다.
- Swift
- Objective-C
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
광고 이벤트 리스너 등록하기
✏️ 참고
광고 참여 상태에 따라 CTA View를 변경하기 위해 이벤트 리스너를 사용했다면, 더 이상 사용할 필요 없습니다.
리워드 적립 결과(BZVRewardResult
) 종류는 리워드 적립 결과(BZVRewardResult
) 종류를 참고하세요.
BZVNativeAd2ViewBinder.subscribeAdEvents()
를 통해 광고 콜백 이벤트를 수신할 수 있습니다. 광고 콜백 이벤트를 수신하려면 bind()
함수를 호출하기 전에 subscribeAdEvents()
함수를 호출해야 합니다.
❗️ 주의
- 광고 이벤트 리스너로 베네핏허브 진입 슬라이드의 노출 또는 클릭 이벤트를 수신할 수는 없습니다.
- 로그 기록, 단순 알림 외에 다른 동작을 추가하는 것을 권장하지 않습니다. 직접 구현한 동작이 네이티브에서 제공하는 기능(광고 자동 갱신 등)과 충돌할 수 있습니다.
다음은 BZVNativeAd2ViewBinder
에서 광고 이벤트를 수신하는 예시입니다.
- Swift
- Objective-C
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
리워드 적립 결과(BZVRewardResult
) 종류
BZVRewardResult | 결과 | 설명 |
---|---|---|
success | 적립성공 | 적립에 성공했습니다. |
alreadyParticipated | 적립 실패 | 이미 적립된 광고입니다. |
missingReward | 적립 실패 | 리워드가 없는 광고입니다. |
tooShortToParticipate | 적립 실패 | 광고 랜딩 페이지에서 너무 빨리 돌아왔습니다. |
timeout | 적립 실패 | 네트워크 타임 아웃이 발생했습니다. |
browserNotLaunched | 적립 실패 | 광고 랜딩 페이지를 보기위한 브라우저가 실행되지 않았습니다. |
networkError | 적립 실패 | 알 수 없는 네트워크 오류가 발생했습니다. |
serverError | 적립 실패 | 알 수 없는 서버 오류가 발생했습니다. |
clientError | 적립 실패 | 알 수 없는 클라이언트 오류가 발생했습니다. |
unknownError | 적립 실패 | 알 수 없는 오류가 발생했습니다. |
클릭 가능 영역 추가하기
기본적으로 클릭이 가능한 MediaView
, CtaView
외에 추가적으로 클릭 가능한 영역을 추가하고 싶은 경우 BZVNativeAd2ViewBinderBuilder
의 clickableViews
를 통해 클릭 가능한 뷰를 추가할 수 있습니다. 다음 예시를 참고하세요.
- Swift
- Objective-C
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) 클릭 가능한 영역을 설정 할 수 있습니다.
}];
캐러셀 구현하기
캐러셀(Carousel)은 여러 개의 광고를 가로로 스크롤 하여 유저에게 노출할 수 있는 UI입니다. Buzzvil SDK를 사용하여 네이티브 지면에 캐러셀을 생성하고 필수 기능을 구현해 캐러셀을 구현할 수 있습니다. 또한 선택에 따라 구현할 수 있는 부가 기능들을 통해 유저 경험을 최적화할 수 있습니다.
✏️ 참고
전체 코드를 확인하려면 샘플 코드를 참고하세요.
캐러셀의 기본 기능 구현하기
1 단계. 광고 할당 받기
네이티브에서 여러 개의 광고를 할당 받기 위해서는 광고 중복 할당을 제어하는 클래스인 BZVNativeAd2Pool
을 사용해야 합니다.
BZVNativeAd2Pool.loadAds()
을 호출하여 현재 할당 받을 수 있는 광고의 갯수를 확인합니다.
✏️ 참고
- 최대 10개의 광고를 요청할 수 있습니다.
- 1개 이상의 광고가 할당된 경우
completionHandler
가 호출됩니다. 할당에 성공한 광고의 갯수는 요청한 광고의 갯수보다 적을 수 있습니다.- 최초 광고 할당 요청에 실패했을 때는
errorHandler
가 호출되고 광고가 노출되지 않습니다. 광고 미할당 시 발생하는NSError
오류 코드에 대한 자세한 내용은 오류 코드가 나타납니다 토픽을 참고하세요.
- Swift
- Objective-C
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
2 단계. UICollectionViewCell 구현하기
Buzzvil SDK에서는 아이템 뷰를 화면에 가로 레이아웃으로 표시하는 UICollectionView
를 사용하여 네이티브 지면에 캐러셀을 구현할 수 있습니다.
UICollectionView
에서 사용할 UICollectionViewCell
을 생성하려면 다음의 절차를 따르세요.
UICollectionViewCell
을 상속받는CarouselCell
을 선언하세요. 광고 레이아웃 구성하기를 참고하여 광고 UI를 구성해주세요.
- Swift
- Objective-C
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
CarouselCell
에BZVNativeAd2ViewBinder
를 선언하세요.BZVNativeAd2View
,BZVMediaView
, 그리고 광고를 보여주기 위해 필요한 뷰를 모두 설정하여BZVNativeAd2ViewBinder
객체를 생성하세요.
- Swift
- Objective-C
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
UICollectionView
index에 해당하는 cell 데이터를 구성할 때 사용할setPool()
메서드와bind()
메서드를CarouselCell
에 구현하세요. Cell을 재사용할 때 이전의 광고 데이터가 남아있지 않도록prepareForReuse()
메서드에서unbind()
메서드를 호출해주세요.
- Swift
- Objective-C
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
3 단계. UICollectionView 구현하기
네이티브 지면의 캐러셀을 위한 UICollectionView
를 생성하려면 다음의 절차를 따르세요.
- 캐러셀을 넣고 싶은 영역에
UICollectionView
를 선언하세요.UICollectionView
의 속성, 뷰 계층, 오토레이아웃을 아래 코드와 같이 구성하세요.
- Swift
- Objective-C
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
UICollectionViewDataSource
의 필수 메서드들을 아래와 같이 구현하세요.collectionView(_:numberOfItemsInSection:)
메서드의 반환값은 실제 할당된 광고의 개수인loadedAdCount
로 설정하세요.collectionView(_:cellForItemAt:)
메서드에서는CarouselCell
을 dequeue 하고setPool()
과bind()
메서드를 순서대로 호출하세요.
- Swift
- Objective-C
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
UICollectionViewFlowLayout
을 선언하세요.UICollectionViewFlowLayout
의 속성을 아래 코드와 같이 구성하고,carouselCollectionView
에 설정하세요.UICollectionViewDelegateFlowLayout
의collectionView(_:layout:sizeForItemAt:)
메서드를 아래와 같이 구현하세요.
- Swift
- Objective-C
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
BZVNativeAd2Pool.loadAds()
의 완료 시점에,reloadData()
를 호출하여carouselCollectionView
를 갱신하세요.
- Swift
- Objective-C
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
4 단계. UIPageControl 구현하기
캐러셀 아이템의 위치를 나타내는 UIPageControl
을 생성하려면 다음의 절차를 따르세요.
UIPageControl
을 선언하세요.UIPageControl
의 속성, 뷰 계층, 오토레이아웃을 아래 코드와 같이 구성하세요.
- Swift
- Objective-C
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
BZVNativeAd2Pool.loadAds()
를 통해 광고를 할당받은 이후,UIPageControl.numberOfPages
를 설정하세요.
- Swift
- Objective-C
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
UIPageControl
의currentPage
를 캐러셀 아이템의 인덱스와 동기화하세요.UIScrollViewDelegate
의scrollViewDidScroll(_:)
메서드를 아래와 같이 구현하세요.
- Swift
- Objective-C
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
이제 앱을 실행하면 캐러셀이 동작하는 모습을 확인할 수 있습니다. 이어서 아래 캐러셀의 필수 기능 구현하기를 참고하여 캐러셀 연동을 완료하세요.
캐러셀의 필수 기능 구현하기
Buzzvil SDK는 다양한 광고 및 유저가 취향에 맞게 광고를 필터링할 수 있는 베네핏허브 지면 진입 경로인 슬라이드 페이지와 진입점을 제공합니다.
✅ 중요
기존에 베네핏허브 지면을 연동하고 있거나 연동할 예정이 있는 경우, SDK 연동의 효용성을 극대화하기 위해 베네핏허브 진입 슬라이드 및 베네핏허브 진입점을 반드시 구현하시기 바랍니다.
베네핏허브 진입 슬라이드
베네핏허브로 진입할 수 있는 슬라이드(Carousel To Feed Slide)를 캐러셀에 구현할 수 있습니다. 캐러셀 내에서 원하는 순서에 베네핏허브 진입 슬라이드를 추가할 수 있으며 추가 갯수 제한도 없어 광고 사이사이에 앱 수익화 효과가 가장 높은 베네핏허브로의 진입점을 자유롭게 추가할 수 있습니다. 이를 위해 Buzzvil SDK는 아래 표에 나열된 클래스를 제공합니다.
코드 | 설명 |
---|---|
BZVFeedPromotionView | 베네핏허브 진입 슬라이드의 View 객체입니다. |
BZVFeedPromotionViewBinder | ViewBinder에 제공된 view들에게 베네핏허브 진입 슬라이드 데이터를 제공하거나 해제합니다.
|
베네핏허브 진입 슬라이드를 구현하려면 다음의 절차를 따르세요.
UICollectionViewCell
을 상속받는FeedPromotionCell
을 선언하세요. UICollectionViewCell 구현하기와 같은 방식으로 뷰를 구성하세요.
- Swift
- Objective-C
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
FeedPromotionCell
에BZVFeedPromotionViewBinder
를 선언하세요.BZVFeedPromotionView
등 베네핏허브 진입 슬라이드를 보여주기 위해 필요한 뷰를 모두 설정하여BZVFeedPromotionViewBinder
객체를 생성하세요.
- Swift
- Objective-C
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
UICollectionView
index에 해당하는 cell 데이터를 구성할 때 사용할 bind()
메서드를 FeedPromotionCell
에 구현하세요. Cell을 재사용할 때 이전의 광고 데이터가 남아있지 않도록 prepareForReuse()
메서드에서 unbind()
메서드를 호출해주세요.
- Swift
- Objective-C
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
- 광고를 표시할 캐러셀
UICollectionView
에FeedPromotionCell
을 추가로 등록하세요.
- Swift
- Objective-C
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
- 광고 할당 받기의
BZVNativeAd2Pool.loadAds()
success 시점에loadedAdCount
값을 조절해 주세요. 아래 예시는 실제로 할당받은 광고의 개수에 베네핏허브 진입 슬라이드 1개를 추가하여loadedAdCount
값을 업데이트하는 예시입니다. 또한, 광고 할당을 실패한BZVNativeAd2Pool.loadAds()
failure 시점에도 베네핏허브 진입 슬라이드 1개가 보이도록 구성합니다.
- Swift
- Objective-C
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
UICollectionViewDataSource
의collectionView(_:cellForItemAt:)
메서드에서 필요한 시점에FeedPromotionCell
을 반환하도록 구현하세요. 아래는 마지막 index를FeedPromotionCell
로 반환하도록 하는 예시입니다.
- Swift
- Objective-C
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
베네핏허브 진입점
캐러셀에서 베네핏허브로 진입할 수 있는 UI인 진입점(Carousel To Feed Link)을 캐러셀의 하단에 구현해야 합니다.
베네핏허브 진입점을 구현하려면 다음의 절차를 따르세요.
BZVFeedEntryView
를 상속받는CarouselFeedEntryView
를 선언하세요.✅ 중요
아래 코드와 같이 앱 UI에 진입 경로 추가를 참고하여CarouselFeedEntryView
를 구현하세요.
- Swift
- Objective-C
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
CarouselCollectionView
아래에CarouselFeedEntryView
를 추가하세요.CarouselFeedEntryView
의 뷰 계층과 오토레이아웃을 적절히 구성하세요.
- Swift
- Objective-C
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
광고 할당 요청에 대한 응답 상태에 따라 베네핏허브 진입점의 표시 여부를 설정하세요.
- 캐러셀 및 베네핏허브 진입점 표시: 보여줄 광고가 있는 경우
- 캐러셀 및 베네핏허브 진입점 미표시: 광고가 아직 로드되지 않은 경우, 오류가 발생하는 경우, 광고가 없는 경우
다음은 CarouselViewController
에서 광고 할당 요청 성공 여부에 따라 CarouselCollectionView
와 CarouselFeedEntryView
의 isHidden
값을 변경하는 예시입니다.
- Swift
- Objective-C
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
베네핏허브 진입점 위치를 고정하세요.
각 광고 아이템의 설명 텍스트(
textDescription
) 길이 차이로 인해 라인 수가 상이하면 각 슬라이드의 베네핏허브 진입점의 위치가 계속 변경되는 현상이 발생할 수 있습니다. 이러한 현상의 발생을 방지하기 위해 광고 설명 텍스트를 표시하는 뷰의numberOfLines
값을 고정하여 항상 일정한 위치를 유지할 수 있습니다.다음은 광고 설명 텍스트를 표시하는 라벨의
numberOfLines
값을 2로 설정한 예시입니다.
- Swift
- Objective-C
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
이제 앱을 실행하면 캐러셀의 마지막 페이지에 베네핏허브 진입 슬라이드가 표시되고, 캐러셀 하단에 베네핏허브 진입점이 추가된 모습을 확인할 수 있습니다. 이어서 아래 캐러셀의 부가 기능 구현하기를 참고하여 유저 친화적인 UI/UX를 제공해보세요.
캐러셀의 부가 기능 구현하기
이 항목에서는 캐러셀에 부가 기능을 구현하기 위한 다양한 방법 중 일부를 제시합니다. 필요에 따라 적절하게 코드를 변형하여 사용해주세요.
무한 루프 구현하기
캐러셀에서 유한한 아이템을 끊임 없이 스크롤할 수 있는 무한 루프 기능을 구현할 수 있습니다. 캐러셀의 무한 루프를 구현하는 방법는 매우 다양하나, 이 가이드에서는 collectionView(_:numberOfItemsInSection:)
에 큰 값을 반환하게 하는 간단한 방법을 제시합니다.
무한 루프를 구현하려면 다음의 절차를 따르세요.
- 캐러셀이 무한한 데이터를 가진 것처럼 표시되도록
UICollectionViewDataSource
의collectionView(_:numberOfItemsInSection:)
메서드를 수정하세요.UIPageControl.numberOfPages
도 동기화해야 합니다.
- Swift
- Objective-C
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
UICollectionViewDataSource
의collectionView(_:cellForItemAt:)
메서드를 아래와 같이 수정하세요. 고유한 cell의 개수로 모듈러 연산하여 index를 적용합니다.
- Swift
- Objective-C
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
BZVNativeAd2Pool.loadAds()
를 통해 광고를 할당받은 이후,UICollectionView
index를 가운데로 이동하세요.
- Swift
- Objective-C
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
로딩 화면 구현하기
BZVNativeAd2ViewBinder.subscribeEvents()
를 통해 광고 참여 후 갱신 시 로딩 화면을 구현하여 네트워크 요청에 따른 딜레이를 시각적으로 안내합니다.
- Swift
- Objective-C
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
광고 이벤트 리스너 등록하기
캐러셀에 포함된 광고가 유저에게 노출되거나 유저가 클릭하는 등의 광고 이벤트가 발생하는 시점을 기록하여 유저 행동을 파악할 수 있습니다.
✏️ 참고
리워드 적립 결과(BZVRewardResult
) 종류는 리워드 적립 결과(BZVRewardResult
) 종류를 참고하세요.
❗️ 주의
- 광고 이벤트 리스너로 베네핏허브 진입 슬라이드의 노출 또는 클릭 이벤트를 수신할 수는 없습니다.
- 로그 기록, 단순 알림 외에 다른 동작을 추가하는 것을 권장하지 않습니다. 직접 구현한 동작이 네이티브에서 제공하는 기능(광고 자동 갱신 등)과 충돌할 수 있습니다.
- Swift
- Objective-C
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
앞뒤 광고 아이템을 부분적으로 노출하기
캐러셀의 앞뒤 아이템이 노출되는 방식으로 구성하려면 다음의 절차를 따르세요.
CarouselCell
의 오토레이아웃을 아래와 같이 변경하세요.
- Swift
- Objective-C
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
CarouselViewController
의flowLayout
과carouselCollectionView
를 아래 코드와 같이 설정하세요.UICollectionView
의isPagingEnabled
는 캐러셀의 앞뒤 아이템이 노출되는 방식으로 구성할 때false
로 설정해야 합니다.
- Swift
- Objective-C
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
UICollectionView
의isPagingEnabled
값을 사용하지 않아서, paging 코드를 직접 구현해야 합니다. 아래 코드와 같이UICollectionViewDelegate
의scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)
메서드를 구현하고,pageControl
index 업데이트 코드도 여기로 이동하세요.
- Swift
- Objective-C
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
UICollectionViewDelegateFlowLayout
의collectionView(_:layout:sizeForItemAt:)
메서드를 아래와 같이 변경하세요.
- Swift
- Objective-C
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
광고 아이템 사이의 여백 조절하기
캐러셀 광고 아이템 사이의 여백을 조절하려면, 앞뒤 광고 아이템을 부분적으로 노출하기에서 설정했던 spacing
값을 적절히 조절합니다.
- Swift
- Objective-C
final class CarouselViewController: UIViewController {
// eg. spacing 값을 12로 설정합니다.
private let spacing = 12
// ...
}
@implementation CarouselViewController
// eg. spacing 값을 12로 설정합니다.
- (NSInteger)spacing {
return 12;
}
// ...
@end