Skip to content
:docs

Native

Learn how to add and configure native placements and implement additional features through the SDK interface.

Key features

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

Integration

Follow these steps to integrate native placements into your app.

Step 1. Prepare

  • Complete Getting started.
  • Obtain a Unit ID for the native placement (referred to below as YOUR_NATIVE_UNIT_ID).

Step 2. Configure the ad layout

See the illustration and table below for the components and UI of a native ad layout.

ComponentDescription
Ad title (required)The ad's title. We recommend no more than 10 characters. You can replace part of the text with an ellipsis as needed, regardless of character count.
Ad creative (required)The ad's image, video, or other creative. You must preserve its aspect ratio., Display it using BuzzMediaView., Size: 1,200 x 627 px, You can add margins. Contact your Buzzvil account manager at help@buzzvil.com for details.
Ad description (required)A detailed description of the ad. We recommend no more than 40 characters. You can replace part of the text with an ellipsis as needed, regardless of character count.
Advertiser icon (required)The advertiser's icon image. You must preserve its aspect ratio., Size: 80 x 80 px
CTA button (required)A button that encourages participation. We recommend no more than 7 characters. You can replace part of the text with an ellipsis as needed, regardless of character count., Display it using the BuzzDefaultCtaView class., To change the button color, see Primary color. For other design changes, see Implement a custom CTA button and replace it with your own design.
BenefitHub entry point (required)A UI element that opens BenefitHub directly when clicked. Design it as your preferred view and add it below the BuzzNativeAdView layout.

Build the native ad layout in a view controller or custom view according to the structure below. Follow these steps to implement a native placement.

The following example configures a view controller according to the native ad layout specifications.

Swift
import UIKit
import BuzzvilSDK

final class ViewController: UIViewController {
  private let nativeAdView = BuzzNativeAdView(frame: .zero)
  private let mediaView = BuzzMediaView(frame: .zero)
  private let iconImageView = UIImageView(frame: .zero)
  private let titleLabel = UILabel(frame: .zero)
  private let descriptionLabel = UILabel(frame: .zero)
  private let ctaView = BuzzDefaultCtaView(frame: .zero) // DefaultBuzzCtaView 대신 자체 구현 CTA 뷰를 사용할 수 있습니다.

  private lazy var benefitHubEntryView: UILabel = {
    let label = UILabel(frame: .zero)
    label.text = "포인트 더 받으러 가기 >"
    
    return label
  }()

  private lazy var viewBinder = BuzzNativeViewBinder.Builder()
    .nativeAdView(nativeAdView)
    .mediaView(mediaView)
    .iconImageView(iconImageView)
    .titleLabel(titleLabel)
    .descriptionLabel(descriptionLabel)
    .ctaView(ctaView)
    .build()
  
  override func viewDidLoad() {
    super.viewDidLoad()

    self.view.addSubview(benefitHubEntryView)
    benefitHubEntryView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBenefitHub)))

    self.view.addSubview(nativeAdView)
    nativeAdView.addSubview(mediaView)
    nativeAdView.addSubview(iconImageView)
    nativeAdView.addSubview(titleLabel)
    nativeAdView.addSubview(descriptionLabel)
    nativeAdView.addSubview(ctaView)

    // AutoLayout Constraints 설정 
    // ...
  }

  @objc
  private func showBenefitHub() {
    let benefitHub = BuzzBenefitHub()
    
    benefitHub.show(on: self)
  }
}

Step 3. Display ads

Follow these steps and the sample code to display ads in the ad layout.

  1. Create a BuzzNative instance and call load() to allocate an ad.
    • Implement the onSuccess and onFailure closures to define behavior for successful and failed allocations.
  2. Pass the BuzzNative instance to bind() of BuzzNativeViewBinder to display the ad in the layout.
    • When the user completes participation, the placement automatically refreshes to the next ad.
  3. (Optional) To update the UI or perform other actions according to the refresh state, implement the onRequest, onSuccess, and onFailure closures as arguments to subscribeRefreshEvents().
    • Call subscribeRefreshEvents() before calling load().
    • Once configured, the same closures are used for subsequent automatic refreshes.
Swift
final class ViewController: UIViewController {
  private lazy var native = BuzzNative(unitId: "YOUR_NATIVE_UNIT_ID")

  private lazy var viewBinder = BuzzNativeViewBinder.Builder()
    .nativeAdView(nativeAdView)
    .mediaView(mediaView)
    .iconImageView(iconImageView)
    .titleLabel(titleLabel)
    .descriptionLabel(descriptionLabel)
    .ctaView(ctaView)
    .build()

  override func viewDidLoad() {
    // Warning: retain cycle 방지를 위해 closure 내에서 반드시 weak self를 사용해주세요.
    native.subscribeRefreshEvents(
      onRequest: { [weak self] in
        // 광고 갱신 할당을 요청한 상태입니다.
        // 이후에는 onSuccess, onFailure 중 하나가 호출됩니다.
        // 광고 자동 갱신을 시도할 때마다 반복적으로 호출됩니다.
      }, 
      onSuccess: { [weak self] nativeAd in
        // 광고 갱신 할당에 성공하면 호출됩니다.
      }, 
      onFailure: { [weak self] error in
        // 더 이상 갱신할 수 있는 광고가 없을 때 호출됩니다.
      }
    )

    // 광고 할당을 수행합니다.
    native.load(
      onSuccess: { [weak self] nativeAd in
        // 초기 광고 할당에 성공하면 호출됩니다.
      }, 
      onFailure: { [weak self] error in
        // 초기 광고 할당에 실패하면 호출됩니다.
      }
    )

    // 할당된 광고 표시를 자동으로 수행합니다.
    viewBinder.bind(native)
  }
}

Ad operation policy (mandatory requirements)

1. Do not refresh ads

  • Make the initial ad request only once when the app screen opens.
  • Subsequent ad requests (reallocations) are allowed only after a clear user action, such as a screen transition.
  • Automatically refreshing ads at fixed intervals or continuously requesting new ads is prohibited.

Excessive ad requests can harm the user experience, ad performance, and revenue.

2. Do not selectively use only impression-based ads

  • Buzzvil combines different ad types to provide an optimized ad experience.
  • API integrations must not selectively use only a specific ad type, such as impression-based ads.
  • SDK integrations must not request ads from another platform when impression-based ads are unavailable.
  • Operate all ad types in balance according to Buzzvil's ad logic.

Restricting or selectively using ad types negatively affects Buzzvil's ad operation strategy and revenue optimization.

Additional features

This section explains how to implement additional native placement features.

Use a native overlay

Native overlays require no separate integration or implementation. When a user returns to your app after participating in a native ad, an overlay appears over the completed ad to encourage entry into BenefitHub. The overlay contains text and an icon, covering the entire native area (NativeAdView) to encourage a click. It disappears automatically when the user clicks Close, or clicks the overlay and then returns.

Overlay UIs adapt to the native placement's size. To increase BenefitHub entry rates, the overlay shows different ad or content product UIs based on the user's past participation and preferences. Each UI type opens the following landing page.

  • Daily Reward: BenefitHub's Daily Reward bottom sheet
    • This does not appear unless Daily Reward is enabled in BenefitHub.
    • See here for details about Daily Reward.
  • Quiz rewards: BenefitHub's content rewards filter
  • Fortune-telling rewards: BenefitHub's content rewards filter
  • Fortune Cookie: BenefitHub's content rewards filter
  • Base reward: BenefitHub
  • This does not appear unless base rewards are enabled in BenefitHub.
  • Total available rewards: BenefitHub
  • Default UI: BenefitHub
    • Appears when a server communication error occurs.

Register an ad event listener

Receive ad callback events through BuzzNative.subscribeAdEvents(). Call subscribeAdEvents() before calling load() to receive them.

The following example receives ad events in BuzzNative.

Swift
final class ViewController: UIViewController {
  // ...생략...

  override func viewDidLoad() {
    // ...생략...

    // Native 광고 이벤트 처리를 위한 closure를 등록하고 각 이벤트에 따라 필요한 기능을 구현합니다.
    // Warning: retain cycle 방지를 위해 closure 내에서 반드시 weak self를 사용해주세요.
    native.subscribeAdEvents(onImpressed: { [weak self] nativeAd in
      // Native 광고가 유저에게 노출되었을 때 호출됩니다.
    }, onClicked: { [weak self] nativeAd in
      // 유저가 Native 광고를 클릭했을 때 호출됩니다.
    }, onRewardRequested: { [weak self] nativeAd in
      // 리워드 적립 요청시에 호출됩니다.
    }, onRewarded: { [weak self] nativeAd, rewardResult in
      // 리워드 적립 결과를 수신했을 때 호출됩니다.
    }, onParticipated: { [weak self] nativeAd in
      // 광고 참여가 완료되었을 때 호출됩니다.
    })

    // ...생략...
  }
}

Reward result types (BuzzRewardResult)

BuzzRewardResultResultDescription
successSuccessThe reward was credited successfully.
alreadyParticipatedFailureThe reward for this ad has already been credited.
missingRewardFailureThe ad has no reward.
tooShortToParticipateFailureThe user returned from the ad landing page too quickly.
connectionTimeoutFailureA network timeout occurred.
browserNotLaunchedFailureThe browser did not launch to display the ad landing page.
networkErrorFailureAn unknown network error occurred.
serverErrorFailureAn unknown server error occurred.
clientErrorFailureAn unknown client error occurred.
unknownFailureAn unknown error occurred.

Implement a carousel

A carousel is a UI that displays multiple ads through horizontal scrolling. Use the Buzzvil SDK to create a carousel in a native placement and implement its required features. Optional features can further improve the user experience.

Implement basic carousel features

Step 1. Allocate ads

To allocate multiple ads in a native placement, use BuzzNativeGroup, which controls duplicate ad allocation.

Call BuzzNativeGroup.load() to check how many ads can currently be allocated.

Swift
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

  // 광고 중복 할당을 막기 위해 하나의 캐러셀에 하나의 BuzzNativeGroup 인스턴스를 생성하여 사용합니다.
  private lazy var nativeGroup = BuzzNativeGroup(unitId: unitId)

  override func viewDidLoad() {
    super.viewDidLoad()
    setupCarousel()
  }

  private func setupCarousel() {
    // BuzzNativeGroup에 광고 할당을 요청합니다.
    nativeGroup.load(
      count: adRequestCount,
      onSuccess: { [weak self] adCount in
        // 실제로 할당받은 광고의 개수(count)를 업데이트합니다.
        self?.loadedAdCount = adCount
      },
      onFailure: { [weak self] error in
        // 광고 할당 실패 시 발생하는 NSError 오류 코드에 대한 자세한 내용은 오류 코드가 나타납니다 토픽을 참고하세요.
      }
    )
  }
}

Step 2. Implement UICollectionViewCell

Use UICollectionView to lay out item views horizontally and implement a carousel in a native placement. Follow these steps to create a UICollectionViewCell for use in UICollectionView.

  1. Declare CarouselCell as a subclass of UICollectionViewCell. Configure the ad UI by following Configure the ad layout.
Swift
import UIKit
import BuzzvilSDK

final class CarouselCell: UICollectionViewCell {
  private lazy var nativeAdView = BuzzNativeAdView(frame: .zero)
  private lazy var mediaView = BuzzMediaView(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 = BuzzDefaultCtaView(frame: .zero)

  override init(frame: CGRect) {
    super.init(frame: frame)
    setupView()
    setupLayout()
  }

  required init?(coder: NSCoder) {
    fatalError()
  }

  private func setupView() {
    contentView.addSubview(nativeAdView)
    nativeAdView.addSubview(mediaView)
    nativeAdView.addSubview(iconImageView)
    nativeAdView.addSubview(titleLabel)
    nativeAdView.addSubview(descriptionLabel)
    nativeAdView.addSubview(ctaView)
  }

  private func setupLayout() {
    // eg. auto layout constraints for nativeAd2View
    nativeAdView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      nativeAdView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
      nativeAdView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16),
      nativeAdView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
      nativeAdView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
    ])

    // AutoLayout Constraints를 설정하세요.
    // ...
  }
}
  1. Declare BuzzNativeViewBinder in CarouselCell. Configure BuzzNativeAdView, BuzzMediaView, and every view needed to display an ad, then create a BuzzNativeViewBinder object.
Swift
final class CarouselCell: UICollectionViewCell {
  private lazy var nativeAdView = BuzzNativeAdView(frame: .zero)
  private lazy var mediaView = BuzzMediaView(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 = BuzzDefaultCtaView(frame: .zero)
  
  // NativeAdView와 하위 컴포넌트를 연결합니다.
  private lazy var viewBinder = BuzzNativeViewBinder.Builder()
    .nativeAdView(nativeAdView)
    .mediaView(mediaView)
    .iconImageView(iconImageView)
    .titleLabel(titleLabel)
    .descriptionLabel(descriptionLabel)
    .ctaView(ctaView)
    .build()
  
  // ...
}
  1. Implement bind() in CarouselCell to configure the cell data for the UICollectionView index. Call unbind() in prepareForReuse() so previous ad data does not remain when a cell is reused.
Swift
final class CarouselCell: UICollectionViewCell {
  // ...

  private lazy var viewBinder = BuzzNativeViewBinder.Builder()
    .nativeAdView(nativeAdView)
    .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 bind(with native: BuzzNative) {
   // bind()를 호출하면 할당된 광고 표시 및 갱신이 자동으로 수행됩니다.
    viewBinder.bind(native)
  }
}

Step 3. Implement UICollectionView

Follow these steps to create UICollectionView for the native placement carousel.

  1. Declare UICollectionView where you want the carousel. Configure the properties, view hierarchy, and Auto Layout of UICollectionView as shown below.
Swift
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()
    setupView()
    setupLayout()
    setupCarousel()
  }

  // ... 
  
  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),
    ])
  }

  // ...
}
  1. Implement the required methods of UICollectionViewDataSource as shown below. Set the return value of collectionView(_:numberOfItemsInSection:) to loadedAdCount, the actual number of allocated ads. In collectionView(_:cellForItemAt:), dequeue CarouselCell and then call bind().
Swift
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()
    }
    
    let native = nativeGroup.natives[indexPath.item]
    cell.bind(with: native)
    return cell
  }
}
  1. Declare UICollectionViewFlowLayout. Configure the properties of UICollectionViewFlowLayout as shown below and set it on carouselCollectionView. Implement collectionView(_:layout:sizeForItemAt:) of UICollectionViewDelegateFlowLayout as shown below.
Swift
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. When BuzzNativeGroup.load() completes, call reloadData() to refresh carouselCollectionView.
Swift
final class CarouselViewController: UIViewController {
  // ...

  private lazy var carouselCollectionView: UICollectionView = {
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
    // ...
    return collectionView
  }()

  private lazy var nativeGroup = BuzzNativeGroup(unitId: unitId)

  override func viewDidLoad() {
    super.viewDidLoad()
    // ...
    setupCarousel()
  }

  // ...

  private func setupCarousel() {
    nativeGroup.load(count: adRequestCount) { [weak self] adCount in
      // ...

      // 광고 할당이 완료되면 carouselCollectionView를 갱신합니다.
      self?.carouselCollectionView.reloadData()
    } errorHandler: { [weak self] error in
      // ...
    }
  }
}

Step 4. Implement UIPageControl

Follow these steps to create a UIPageControl that indicates the carousel item's position.

  1. Declare UIPageControl. Configure the properties, view hierarchy, and Auto Layout of UIPageControl as shown below.
Swift
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. After allocating ads through BuzzNativeGroup.load(), set UIPageControl.numberOfPages.
Swift
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() {
    nativeGroup.load(count: adRequestCount) { [weak self] adCount in
      // ...
      self?.pageControl.numberOfPages = adCount
    } errorHandler: { [weak self] error in
      // ...
    }
  }
}
  1. Synchronize currentPage of UIPageControl with the carousel item index. Implement scrollViewDidScroll(_:) of UIScrollViewDelegate as shown below.
Swift
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)
  }
}

Run the app to see the carousel in action. Then complete the integration by following Implement required carousel features below.

Implement required carousel features

The Buzzvil SDK provides a slide page and entry point into BenefitHub, where users can find a variety of ads and filter them to suit their preferences.

BenefitHub entry point

Implement an entry point above or below the carousel so users can open BenefitHub from it.

Swift

private lazy var benefitHubEntryView: UILabel = {
  let label = UILabel(frame: .zero)
  label.text = "포인트 더 받으러 가기 >"

  label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBenefitHub)))
  
  return label
}()

@objc
private func showBenefitHub() {
  let benefitHub = BuzzBenefitHub()
  
  benefitHub.show(on: self)
}

Implement optional carousel features

This section presents some of the ways you can add features to a carousel. Adapt the code to your needs.

Implement infinite scrolling

You can implement an infinite loop that lets users scroll continuously through a finite set of carousel items. There are many approaches; this guide uses a simple method in which collectionView(_:numberOfItemsInSection:) returns a large value.

Follow these steps to implement an infinite loop.

  1. Modify collectionView(_:numberOfItemsInSection:) of UICollectionViewDataSource to make the carousel appear to have infinite data. You must also synchronize UIPageControl.numberOfPages.
Swift
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() {
    nativeGroup.load(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
  }
  
  // ...
}
  1. Modify collectionView(_:cellForItemAt:) of UICollectionViewDataSource as shown below. Use modulo with the number of unique cells to calculate the index.
Swift
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를 적용합니다.
    let native = nativeGroup.natives[indexPath.item % loadedAdCount]
    cell.bind(with: native)
    return cell
  }
}
  1. After allocating ads through BuzzNativeGroup.load(), move the UICollectionView index to the middle.
Swift
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() {
    nativeGroup.load(count: adRequestCount) { [weak self] count in
      // ...
      self?.loadedAdCount = count 
      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
  }
}

Implement a loading screen

Use BuzzNative.subscribeRefreshEvents() to show a loading screen during refreshes after ad participation, visually indicating network request delays.

Swift
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 bind(with native: BuzzNative) {
    native.subscribeRefreshEvents(
      onRequest: { [weak self] in
        self?.activityIndicatorView.startAnimating()
        self?.nativeAdView.alpha = 0.5
      },
      onSuccess: { [weak self] _ in
        self?.activityIndicatorView.stopAnimating()
        self?.nativeAdView.alpha = 1
      },
      onFailure: { [weak self] error in
        self?.activityIndicatorView.stopAnimating()
        self?.nativeAdView.alpha = 1
      },
    )

    viewBinder.bind(native: native)
  }
}

extension CarouselViewController: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    // ...
    cell.bind(with: native)
    return cell
  }
}

Register an ad event listener

Record when carousel ad events occur, such as impressions and clicks, to understand user behavior.

Swift
final class CarouselCell: UICollectionViewCell {
  // ...
  
  func bind(with native: BuzzNative) {
    // ...

    native.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.bind(with: native)
    return cell
  }
}

Partially show adjacent ad items

Follow these steps to show the previous and next carousel items partially.

  1. Change the Auto Layout of CarouselCell as shown below.
Swift
final class CarouselCell: UICollectionViewCell {
  private lazy var nativeAdView = BuzzNativeAdView(frame: .zero)
  
  // ...

  override init(frame: CGRect) {
    super.init(frame: frame)
    // ...
    setupLayout()
    // ...
  }
  
  private func setupLayout() {
    // CarouselCell과 동일한 크기로 설정합니다.
    nativeAdView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      nativeAdView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
      nativeAdView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
      nativeAdView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
      nativeAdView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
    ])

    // ...
  }
  
  // ...
}
  1. Set flowLayout and carouselCollectionView of CarouselViewController as shown below. Set isPagingEnabled of UICollectionView to false when configuring the carousel to show adjacent items.
Swift
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
  }()
  
  // ...
}
  1. Because this does not use the isPagingEnabled value of UICollectionView, implement paging yourself. Implement scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) of UICollectionViewDelegate as shown below and move the code that updates the pageControl index here as well.
Swift
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: 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. Modify collectionView(_:layout:sizeForItemAt:) of UICollectionViewDelegateFlowLayout as shown below.
Swift
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)
  }
}

Adjust spacing between ad items

To adjust spacing between carousel ad items, change the spacing value configured in Partially show adjacent ad items.

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

  // ...
}

Frequently asked questions

QuestionAnswer
Why must I add a user support button for action-based ads?If you allocate action-based ads, you must implement user support to operate the service reliably. See Add user support for action-based ads.