본문으로 건너뛰기

캐러셀 구현

캐러셀(Carousel)은 여러 개의 광고를 가로로 스크롤 하여 사용자에게 노출할 수 있는 UI입니다. BuzzAd iOS용 SDK를 사용하여 네이티브 2.0 지면에 캐러셀을 생성하고 필수 기능을 추가해 캐러셀을 구현할 수 있습니다. 또한 선택에 따라 구현할 수 있는 부가 기능들을 통해 사용자 경험을 최적화할 수 있습니다.

✏️ 참고
전체 코드를 확인하려면 샘플 앱을 참고하세요.

캐러셀의 기본 기능 구현하기

광고 할당 받기

네이티브 2.0에서 여러 개의 광고를 할당받기 위해서는 광고 중복 할당을 제어하는 클래스인 BZVNativeAd2Pool을 사용해야 합니다.

BZVNativeAd2Pool.loadAds()을 호출하여 광고 할당을 요청하고, 실제로 할당받은 광고의 개수를 확인합니다.

✏️ 참고

  • 최대 10개의 광고를 요청할 수 있습니다.
  • 1개 이상의 광고가 할당된 경우 completionHandler가 호출됩니다. 할당에 성공한 광고의 개수는 요청한 광고의 개수보다 적을 수 있습니다.
  • 최초 광고 할당 요청에 실패했을 때는 errorHandler가 호출되고 광고가 노출되지 않습니다. 광고 미할당 시 발생하는 NSError 오류 코드에 대한 자세한 내용은 오류 코드가 나타납니다 토픽을 참고하세요.
import UIKit
import BuzzAdBenefit

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 오류 코드에 대한 자세한 내용은 오류 코드가 나타납니다 토픽을 참고하세요.
}
}
}

UICollectionViewCell 구현하기

BuzzAd iOS용 SDK에서는 아이템 뷰를 화면에 가로 레이아웃으로 표시하는 UICollectionView를 사용하여 네이티브 2.0 지면에 캐러셀을 구현할 수 있습니다. UICollectionView에서 사용할 UICollectionViewCell을 생성하려면 다음의 절차를 따르세요.

  1. UICollectionViewCell을 상속받는 CarouselCell을 선언하세요. Native 2.0 기본 설정 - 광고 레이아웃 구성하기를 참고하여 광고 UI를 구성해주세요.
import UIKit
import BuzzAdBenefit

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를 설정하세요.
// ...
}
}
  1. CarouselCellBZVNativeAd2ViewBinder를 선언하세요. BZVNativeAd2View, BZVMediaView, 그리고 광고를 보여주기 위해 필요한 뷰를 모두 설정하여 BZVNativeAd2ViewBinder 객체를 생성하세요.
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()

// ...
}
  1. UICollectionView index에 해당하는 cell 데이터를 구성할 때 사용할 setPool() 메서드와 bind() 메서드를 CarouselCell에 구현하세요. Cell을 재사용할 때 이전의 광고 데이터가 남아있지 않도록 prepareForReuse() 메서드에서 unbind() 메서드를 호출해주세요.
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()
}
}

UICollectionView 구현하기

네이티브 2.0 지면의 캐러셀을 위한 UICollectionView를 생성하려면 다음의 절차를 따르세요.

  1. 캐러셀을 넣고 싶은 영역에 UICollectionView를 선언하세요. UICollectionView의 속성, 뷰 계층, 오토레이아웃을 아래 코드와 같이 구성하세요.
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() {
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.65),
])
}

// ...
}
  1. UICollectionViewDataSource의 필수 메서드들을 아래와 같이 구현하세요. collectionView(_:numberOfItemsInSection:) 메서드의 반환값은 실제 할당된 광고의 개수인 loadedAdCount로 설정하세요. collectionView(_:cellForItemAt:) 메서드에서는 CarouselCell을 dequeue 하고 setPool()bind() 메서드를 순서대로 호출하세요.
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
}
}
  1. UICollectionViewFlowLayout을 선언하세요. UICollectionViewFlowLayout의 속성을 아래 코드와 같이 구성하고, carouselCollectionView에 설정하세요. UICollectionViewDelegateFlowLayoutcollectionView(_:layout:sizeForItemAt:) 메서드를 아래와 같이 구현하세요.
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)
}
}
  1. BZVNativeAd2Pool.loadAds()의 완료 시점에, reloadData()를 호출하여 carouselCollectionView를 갱신하세요.
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
// ...
}
}
}

UIPageControl 구현하기

캐러셀 아이템의 위치를 나타내는 UIPageControl을 생성하려면 다음의 절차를 따르세요.

  1. UIPageControl을 선언하세요. UIPageControl의 속성, 뷰 계층, 오토레이아웃을 아래 코드와 같이 구성하세요.
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),
])
}
}
  1. BZVNativeAd2Pool.loadAds()를 통해 광고를 할당받은 이후, UIPageControl.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
// ...
}
}
}
  1. UIPageControlcurrentPage를 캐러셀 아이템의 인덱스와 동기화하세요. UIScrollViewDelegatescrollViewDidScroll(_:) 메서드를 아래와 같이 구현하세요.
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)
}
}

이제 앱을 실행하면 캐러셀이 동작하는 모습을 확인할 수 있습니다. 이어서 아래 캐러셀의 필수 기능 구현하기를 참고하여 캐러셀 연동을 완료하세요.

캐러셀의 필수 기능 구현하기

BuzzAd iOS용 SDK는 다양한 광고 및 사용자가 취향에 맞게 광고를 필터링할 수 있는 피드 지면 진입 경로인 슬라이드 페이지와 엔트리 포인트를 제공합니다.

 중요
기존에 피드 지면을 연동하고 있거나 연동할 예정이 있는 경우, SDK 연동의 효용성을 극대화하기 위해 피드 진입 슬라이드 및 피드 엔트리 포인트를 반드시 구현하시기 바랍니다.

피드 진입 슬라이드

피드로 진입할 수 있는 슬라이드(Carousel To Feed Slide)를 캐러셀에 구현할 수 있습니다. 캐러셀 내에서 원하는 순서에 피드 진입 슬라이드를 추가할 수 있으며 추가 개수 제한도 없어 광고 사이사이에 앱 수익화 효과가 가장 높은 피드로의 진입점을 자유롭게 추가할 수 있습니다. 이를 위해 BuzzAd iOS용 SDK는 아래 표에 나열된 클래스를 제공합니다.

코드설명
BZVFeedPromotionView피드 진입 슬라이드의 View 객체입니다.
BZVFeedPromotionViewBinderViewBinder에 제공된 view들에게 피드 진입 슬라이드 데이터를 제공하거나 해제합니다.
  • bind(): 제공된 view들에게 피드 진입 슬라이드 데이터를 제공합니다.
  • unbind(): 제공된 view들에게서 피드 진입 슬라이드 데이터를 해제합니다.
  • Builder: BZVFeedPromotionViewBinder 객체를 생성할 때 사용하는 빌더입니다.

피드 진입 슬라이드를 구현하려면 다음의 절차를 따르세요.

  1. UICollectionViewCell을 상속받는 FeedPromotionCell을 선언하세요. UICollectionViewCell 구현하기와 같은 방식으로 뷰를 구성하세요.
import UIKit
import BuzzAdBenefit

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를 설정하세요.
// ...
}
}
  1. FeedPromotionCellBZVFeedPromotionViewBinder를 선언하세요. BZVFeedPromotionView 등 피드 진입 슬라이드를 보여주기 위해 필요한 뷰를 모두 설정하여 BZVFeedPromotionViewBinder 객체를 생성하세요.
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()

// ...
}
  1. UICollectionView index에 해당하는 cell 데이터를 구성할 때 사용할 bind() 메서드를 FeedPromotionCell에 구현하세요. Cell을 재사용할 때 이전의 광고 데이터가 남아있지 않도록 prepareForReuse() 메서드에서 unbind() 메서드를 호출해주세요.
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()
}
}
  1. 광고를 표시할 캐러셀 UICollectionViewFeedPromotionCell을 추가로 등록하세요.
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
}()

// ...
}
  1. 광고 할당 받기BZVNativeAd2Pool.loadAds() success 시점에 loadedAdCount 값을 조절해 주세요. 아래 예시는 실제로 할당받은 광고의 개수에 피드 진입 슬라이드 1개를 추가하여 loadedAdCount 값을 업데이트하는 예시입니다. 또한, 광고 할당을 실패한 BZVNativeAd2Pool.loadAds() failure 시점에도 피드 진입 슬라이드 1개가 보이도록 구성합니다.
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 + feedPromotionSlideCount
self?.carouselCollectionView.reloadData()
} errorHandler: { [weak self] error in
// ...
// 광고 할당이 실패했을 때, 피드 진입 슬라이드 1개가 보이도록 구성합니다.
self?.loadedAdCount = feedPromotionSlideCount
self?.carouselCollectionView.reloadData()
}
}
}
  1. UICollectionViewDataSourcecollectionView(_:cellForItemAt:) 메서드에서 필요한 시점에 FeedPromotionCell을 반환하도록 구현하세요. 아래는 마지막 index를 FeedPromotionCell로 반환하도록 하는 예시입니다.
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
}
}
}

피드 엔트리 포인트

캐러셀에서 피드로 진입할 수 있는 UI인 엔트리 포인트(Carousel To Feed Link)를 캐러셀의 하단에 구현해야 합니다.

피드 엔트리 포인트를 구현하려면 다음의 절차를 따르세요.

  1. BZVFeedEntryView를 상속받는 CarouselFeedEntryView를 선언해야 합니다. 아래 코드와 같이 앱 UI에 진입 경로 추가를 참고하여 CarouselFeedEntryView를 구현하세요.
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),
])
}
}
  1. CarouselCollectionView 아래에 CarouselFeedEntryView를 추가하세요. CarouselFeedEntryView의 뷰 계층과 오토레이아웃을 적절히 구성하세요.
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를 설정하세요.
// ...
}
}
  1. 광고 할당 요청에 대한 응답 상태에 따라 피드 엔트리 포인트의 표시 여부를 설정하세요.

    • 캐러셀 및 피드 엔트리 포인트 표시: 보여줄 광고가 있는 경우
    • 캐러셀 및 피드 엔트리 포인트 미표시: 광고가 아직 로드되지 않은 경우, 오류가 발생하는 경우, 광고가 없는 경우

다음은 CarouselViewController에서 광고 할당 요청 성공 여부에 따라 CarouselCollectionViewCarouselFeedEntryViewisHidden 값을 변경하는 예시입니다.

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
}
}
}
  1. 광고 아이템의 높이 차이와 상관없이 피드 엔트리 포인트 위치 고정하기

각 광고 아이템의 설명 텍스트(textDescription) 길이 차이로 인해 라인 수가 상이하면 각 슬라이드의 피드 엔트리 포인트의 위치가 계속 변경되는 현상이 발생할 수 있습니다. 이러한 현상의 발생을 방지하기 위해 광고 설명 텍스트를 표시하는 뷰의 numberOfLines 값을 고정하여 항상 일정한 위치를 유지할 수 있습니다.

다음은 광고 설명 텍스트를 표시하는 라벨의 numberOfLines 값을 2로 설정한 예시입니다.

final class CarouselCell: UICollectionViewCell {
// ...

private lazy var descriptionLabel: UILabel = {
let label = UILabel(frame: .zero)
label.numberOfLines = 2
return label
}()

// ...
}

이제 앱을 실행하면 캐러셀의 마지막 페이지에 피드 진입 슬라이드가 표시되고, 캐러셀 하단에 피드 진입점이 추가된 모습을 확인할 수 있습니다. 이어서 아래 캐러셀의 부가 기능 구현하기를 참고하여 유저 친화적인 UI/UX를 제공해 보세요.

캐러셀의 부가 기능 구현하기

이 항목에서는 캐러셀에 부가 기능을 구현하기 위한 다양한 방법 중 일부를 제시합니다. 필요에 따라 적절하게 코드를 변형하여 사용해 주세요.

무한 루프 구현하기

캐러셀에서 유한한 아이템을 끊임없이 스크롤 할 수 있는 무한 루프 기능을 구현할 수 있습니다. 캐러셀의 무한 루프를 구현하는 방법은 매우 다양하나, 이 가이드에서는 collectionView(_:numberOfItemsInSection:)에 큰 값을 반환하게 하는 간단한 방법을 제시합니다.

무한 루프를 구현하려면 다음의 절차를 따르세요.

  1. 캐러셀이 무한한 데이터를 가진 것처럼 표시되도록 UICollectionViewDataSourcecollectionView(_:numberOfItemsInSection:) 메서드를 수정하세요. UIPageControl.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 = infiniteItemCount
} errorHandler: { [weak self] error in
// ...
}
}
}

extension CarouselViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return infiniteItemCount
}

// ...
}
  1. UICollectionViewDataSourcecollectionView(_:cellForItemAt:) 메서드를 아래와 같이 수정하세요. 고유한 cell의 개수로 모듈러 연산하여 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
}
}
  1. BZVNativeAd2Pool.loadAds()를 통해 광고를 할당받은 이후, UICollectionView index를 가운데로 이동하세요.
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
}
}

로딩 화면 구현하기

BZVNativeAd2ViewBinder.subscribeEvents()를 통해 광고 참여 후 갱신 시 로딩 화면을 구현하여 네트워크 요청에 따른 딜레이를 시각적으로 안내합니다.

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?.nativeAdView.alpha = 1
}, onError: { [weak self] in
self?.activityIndicatorView.stopAnimating()
self?.nativeAdView.alpha = 1
print("error: \($0)")
}, onCompleted: { [weak self] in
self?.activityIndicatorView.stopAnimating()
self?.nativeAdView.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
}
}

광고 이벤트 리스너 등록하기

캐러셀에 포함된 광고가 사용자에게 노출되거나 사용자가 클릭하는 등의 광고 이벤트가 발생하는 시점을 기록하여 유저 행동을 파악할 수 있습니다.

⚠️ 주의

  • 로그 기록, 단순 알림 외에 다른 동작을 추가하는 것을 권장하지 않습니다. 직접 구현한 동작이 네이티브 2.0의 기능(자동 갱신 등)과 충돌할 수 있습니다.
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
}
}

앞뒤 광고 아이템을 부분적으로 노출하기

캐러셀의 앞뒤 아이템이 노출되는 방식으로 구성하려면 다음의 절차를 따르세요.

  1. CarouselCell의 오토레이아웃을 아래와 같이 변경하세요.
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),
])

// ...
}

// ...
}
  1. CarouselViewControllerflowLayoutcarouselCollectionView를 아래 코드와 같이 설정하세요. UICollectionViewisPagingEnabled는 캐러셀의 앞뒤 아이템이 노출되는 방식으로 구성할 때 false로 설정해야 합니다.
final class CarouselViewController: UIViewController {
private let spacing = 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
}()

// ...
}
  1. UICollectionViewisPagingEnabled 값을 사용하지 않아서, paging 코드를 직접 구현해야 합니다. 아래 코드와 같이 UICollectionViewDelegatescrollViewWillEndDragging(_:withVelocity:targetContentOffset:) 메서드를 구현하고, pageControl index 업데이트 코드도 여기로 이동하세요.
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)
}
}
  1. UICollectionViewDelegateFlowLayoutcollectionView(_:layout:sizeForItemAt:) 메서드를 아래와 같이 변경하세요.
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)
}
}

광고 아이템 사이의 여백 조절하기

캐러셀 광고 아이템 사이의 여백을 조절하려면, 앞뒤 광고 아이템을 부분적으로 노출하기에서 설정했던 spacing 값을 적절히 조절합니다.

final class CarouselViewController: UIViewController {
// eg. spacing 값을 12로 설정합니다.
private let spacing = 12

// ...
}