iOS/SwiftUI

[ SwiftUI ] 광고배너 만들기

Sheep1sik 2024. 6. 27. 22:44
반응형

SwiftUI를 통해 광고배너를 만들어봤습니다.

먼저 광고배너를 만들기 위해서 TabView를 채택하여 구현을 했고 구현 목표는 아래와 같았습니다.

 

  • 3초간격으로 화면 이동
  • 무한적인 광고배너 ( 1 -> 2 -> 3 -> 1 -> 2-> . . .)

먼저 무한적인 광고배너를 만들기 위해 아래의 글을 참고해서 구현했습니다.

 

Bidirectional infinite PageView in SwiftUI

I'm trying to make a bidirectional TabView (with .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))) whose datasource will change over time. Below is the code that describes what is expected...

stackoverflow.com


간단 원리 설명

 

  • InfinitePageView는 TabView를 사용하여 세 개의 뷰(이전, 현재, 다음)를 순환합니다.
  • 사용자가 스와이프하여 탭을 변경하면 currentTab 상태 변수가 업데이트됩니다.
  • currentTab이 0이 아니면 onDisappear 메서드가 호출되어 selection을 업데이트하고 currentTab을 0으로 초기화합니다.
  • 이 과정은 이전, 현재, 다음 항목을 동적으로 계산하여 무한히 스크롤할 수 있게 합니다.
  • 스와이프가 매우 빠르게 일어날 때 발생할 수 있는 글리치를 방지하기 위해 currentTab이 0이 아닐 때 스와이프를 비활성화합니다.

코드 설명을 위해 주석문을 달아놨습니다.

 

InfinitePageView

// InfinitePageView는 주어진 선택 항목(T)을 기반으로 이전 및 다음 항목을 보여주는 무한 스크롤 뷰입니다.
// C는 각 페이지의 내용을 나타내는 뷰의 유형입니다. T는 선택 항목의 타입으로 Hashable을 준수해야 합니다.
struct InfinitePageView<C, T>: View where C: View, T: Hashable {
    // 현재 선택된 항목을 바인딩으로 받아옵니다.
    @Binding var selection: T

    // 주어진 선택 항목에서 이전 항목을 계산하는 함수입니다.
    let before: (T) -> T
    // 주어진 선택 항목에서 다음 항목을 계산하는 함수입니다.
    let after: (T) -> T

    // 주어진 선택 항목을 기반으로 뷰를 생성하는 클로저입니다.
    @ViewBuilder let view: (T) -> C

    // 현재 탭의 인덱스를 저장하는 상태 변수입니다.
    @State private var currentTab: Int = 0

    // 뷰의 본체입니다.
    var body: some View {
        // 이전 및 다음 선택 항목을 계산합니다.
        let previousIndex = before(selection)
        let nextIndex = after(selection)
        
        // TabView를 생성하여 선택 항목의 이전, 현재, 다음 항목을 표시합니다.
        TabView(selection: $currentTab) {
            // 이전 선택 항목을 표시하는 뷰입니다.
            view(previousIndex)
                .tag(-1)

            // 현재 선택 항목을 표시하는 뷰입니다.
            view(selection)
                .onDisappear() {
                    // 현재 탭이 변경될 때 선택 항목을 업데이트합니다.
                    if currentTab != 0 {
                        selection = currentTab < 0 ? previousIndex : nextIndex
                        currentTab = 0
                    }
                }
                .tag(0)

            // 다음 선택 항목을 표시하는 뷰입니다.
            view(nextIndex)
                .tag(1)
        }
        // 페이지 인디케이터를 숨기고 페이지 스타일로 TabView를 설정합니다.
        .tabViewStyle(.page(indexDisplayMode: .never))
        // 탭이 0이 아닐 때 스와이프를 비활성화하여 빠른 스와이프 시 발생하는 글리치를 방지합니다.
        .disabled(currentTab != 0) // FIXME: workaround to avoid glitch when swiping twice very quickly
    }
}

 

ADBannerViewModel

import SwiftUI
import Combine

// ADBannerViewModel은 광고 배너의 상태를 관리하는 뷰 모델입니다.
class ADBannerViewModel: ObservableObject {
    // 현재 선택된 광고 인덱스를 퍼블리시하여 변경 사항을 구독할 수 있게 합니다.
    @Published var adIndex = 0
    
    // 광고 이미지를 제공하는 모델 인스턴스를 생성합니다.
    private let model = ADBannerModel()
    // 타이머를 관리하기 위한 변수입니다.
    private var timer: Timer?
    
    // 광고 이미지 배열을 반환하는 계산 프로퍼티입니다.
    var adImages: [String] {
        return model.adImages
    }

    // 주어진 인덱스를 올바른 범위로 수정하는 함수입니다.
    // 광고 이미지 배열의 길이에 맞춰 인덱스를 순환시킵니다.
    func correctedIndex(for index: Int) -> Int {
        let count = model.adImages.count
        return (count + index) % count
    }

    // 자동 스크롤을 시작하는 함수입니다.
    func startAutoScroll() {
        // 기존 타이머가 있으면 중지합니다.
        stopAutoScroll()
        // 3초마다 반복되는 타이머를 설정합니다.
        timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
            // self가 nil이 아니면 강한 참조로 전환하여 사용합니다.
            guard let self = self else { return }
            // 광고 인덱스를 다음 인덱스로 업데이트합니다.
            self.adIndex = self.correctedIndex(for: self.adIndex + 1)
        }
    }
    
    // 자동 스크롤을 중지하는 함수입니다.
    func stopAutoScroll() {
        // 타이머를 무효화하고 nil로 설정합니다.
        timer?.invalidate()
        timer = nil
    }
}

 

ADBannerView

import SwiftUI

// ADBannerView 구조체

// ADBannerView는 광고 배너를 무한 스크롤로 보여주는 뷰입니다.
struct ADBannerView: View {
    // ADBannerViewModel 인스턴스를 상태 객체로 선언하여 뷰 모델을 관리합니다.
    @StateObject private var viewModel = ADBannerViewModel()
    
    var body: some View {
        // InfinitePageView를 사용하여 광고 이미지를 무한 스크롤로 보여줍니다.
        InfinitePageView(
            // 현재 선택된 광고 인덱스를 바인딩합니다.
            selection: $viewModel.adIndex,
            // 이전 광고 인덱스를 계산하는 클로저입니다.
            before: { viewModel.correctedIndex(for: $0 - 1) },
            // 다음 광고 인덱스를 계산하는 클로저입니다.
            after: { viewModel.correctedIndex(for: $0 + 1) },
            // 주어진 인덱스에 해당하는 광고 이미지를 보여주는 뷰를 생성하는 클로저입니다.
            view: { index in
                ZStack {
                    withAnimation {
                        // 인덱스에 해당하는 광고 이미지를 표시합니다.
                        Image(viewModel.adImages[index])
                            .resizable()
                    }
                    // 광고 배너 항목 뷰를 추가하여 현재 페이지 인덱스와 총 페이지 수를 표시합니다.
                    ADBannerItemView(pageIndex: $viewModel.adIndex, pageTotal: .constant(viewModel.adImages.count))
                }
            }
        )
        // 광고 배너 뷰의 높이를 170으로 설정합니다.
        .frame(height: 170)
        // 뷰가 나타날 때 자동 스크롤을 시작합니다.
        .onAppear {
            viewModel.startAutoScroll()
        }
        // 뷰가 사라질 때 자동 스크롤을 중지합니다.
        .onDisappear {
            viewModel.stopAutoScroll()
        }
    }
}

 

ADBannerItemView

import SwiftUI

// ADBannerItemView는 현재 광고 배너의 페이지 인덱스와 총 페이지 수를 표시하는 뷰입니다.
struct ADBannerItemView: View {
    // 현재 페이지 인덱스를 바인딩으로 받아옵니다.
    @Binding var pageIndex: Int
    // 총 페이지 수를 바인딩으로 받아옵니다.
    @Binding var pageTotal: Int
    
    var body: some View {
        VStack {
            HStack {
                // 광고 배너(AD) 숫자 표시를 위한 캡슐 형태의 배경을 생성합니다.
                Capsule()
                    .foregroundColor(Color(.secondarySystemBackground)) // 배경 색상 설정
                    .frame(width: 40, height: 15) // 캡슐의 크기 설정
                    .padding() // 캡슐 주위에 패딩을 추가
                    .overlay(
                        // 페이지 수 계산 및 텍스트로 표시
                        Text("\(pageIndex+1)/\(pageTotal)") // 현재 페이지 인덱스와 총 페이지 수를 표시
                            .foregroundColor(.primary) // 텍스트 색상 설정
                            .font(.system(size: 10)) // 텍스트 폰트 크기 설정
                    )
                Spacer() // 왼쪽에 배치된 요소와 오른쪽에 배치될 요소 사이에 여백 추가
            }
            Spacer() // 상단에 배치된 요소와 하단에 배치될 요소 사이에 여백 추가
        }
    }
}

 

 

구현 영상

 

반응형