[SwiftUI] Infinite Carousel ๊ตฌํ˜„ํ•˜๊ธฐ 2 (feat.Timer)

2024. 9. 11. 09:25ยท๐ŸŽ Dev/๊ตฌํ˜„

 

https://calliek.tistory.com/62

 

[SwiftUI] TabView + DragGesture๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค ํƒ€์ด๋จธ ์กฐ์ ˆํ•˜๊ธฐ

ํ”„๋กœ๋ชจ์…˜ ํŒ์—… ํ™”๋ฉด์„ ๋‹ด๋‹นํ•ด์„œ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ๋ฌดํ•œ ์Šคํฌ๋กค(์ด๋ฏธ์ง€ ๋ฐฐ์—ด ์ˆœํ™˜) + ํƒ€์ด๋จธ ์ž‘๋™์ด ๊ฐ€๋Šฅํ† ๋ก ๊ตฌํ˜„์„ ํ•ด์•ผํ–ˆ๋‹ค. - ๋‚ด๊ฐ€ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•œ ๊ฒƒ: 1. ์ฒซ๋ฒˆ์งธ ์ธ๋ฑ์Šค ์ด๋ฏธ์ง€์—์„œ ์™ผ์ชฝ์œผ๋กœ ์Šค์™€

calliek.tistory.com

 

๋ฌธ์ œ์ :

 

- ๋ถˆ๊ณผ ์ผ์ฃผ์ผ ์ „ ๊ฐ™์€ ๋งฅ๋ฝ์˜ ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ–ˆ์—ˆ๋Š”๋ฐ, ํ•ด๋‹น ๋ฐฉ์‹๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ํƒ€์ด๋จธ๊ฐ€ ์ž‘๋™ ๋  ๋•Œ index ๋ณ€๊ฒฝ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ์ด ์•ˆ ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค.

- ๊ทธ๋ž˜์„œ ํ•ด๋‹น ๊ธ€์— ์ถ”๊ฐ€ํ•ด๋‘์—ˆ๋˜ ๋ธ”๋กœ๊ทธ๊ธ€๋กœ ์ „๋ฉด ์ˆ˜์ •์„ ํ•ด๋ณด์•˜์œผ๋‚˜, ๋“œ๋ž˜๊ทธ๋ฅผ ํ•  ๋•Œ ํ•ด๋‹น ์ธ๋ฑ์Šค์˜ ํ™”๋ฉด์ด ๋“œ๋ž˜๊ทธ ๋˜๋Š” ๋ชจ์Šต์ด ๋ณด์ด์ง€ ์•Š๊ณ  ์•ก์…˜์ด ๋‹ค ๋๋‚˜๊ณ  ์Šฌ๋ผ์ด๋“œ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ํ™”๋ฉด์ด ์ „ํ™˜๋˜์—ˆ๋‹ค.

 

ํ•ด๊ฒฐ:

 

- ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค ์ฝ”๋“œ๋ฅผ ์ปค์Šคํ…€ํ•˜๋Š” ๋Œ€์‹  ๊ทธ๋ƒฅ ๋‚ด๊ฐ€ ์ปค์Šคํ…€ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค^^...

 

 

 

๊ตฌํ˜„ (ํƒ€์ด๋จธ X, ์˜ค์ง infinite Carousel):

 

 

 

 

1. ํŽ˜์ดํฌ ์•„์ดํ…œ์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค. 

 

/// ํŽ˜์ดํฌ ์•„์ดํ…œ ์ถ”๊ฐ€
  private func configureCircularItemList() {
    // ์‹œ์ž‘๊ณผ ๋์— ๋งˆ์ง€๋ง‰, ์ฒซ ๋ฒˆ์งธ ์ƒ‰์ƒ ์ถ”๊ฐ€
    colors.insert(colors[colors.count - 1], at: 0)
    colors.append(colors[1])
  }

 

- UIKit์œผ๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•œ ์‚ฌ๋žŒ๋“ค์„ ๋ณด๋‹ˆ๊นŒ, ์ˆœํ™˜ ๋Œ ๋ฐฐ์—ด์˜ 0๋ฒˆ์งธ์™€ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค์˜ ์•ž ๋’ค๋กœ ๋งˆ์ง€๋ง‰, 0๋ฒˆ์งธ ์•„์ดํ…œ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ  ๊ฐ๊ฐ ๋ฌดํ•œ์Šคํฌ๋กค์ด ๋˜์–ด์•ผ ํ•˜๋Š” ์ธ๋ฑ์Šค์— ๋„๋‹ฌํ•˜๋ฉด ๋ณด์—ฌ์ค˜์•ผํ•˜๋Š” ์ธ๋ฑ์Šค๋ฅผ ๋‹ฌ๋ฆฌ ํ•˜๋Š” ์ž‘์€(?) ๋ˆˆ์†์ž„ ๋ฐฉ์‹์„ ํƒํ–ˆ๋‹ค.

 

 

2.  ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์œ„ํ•œ ํŽ˜์ดํฌ ์ธ๋ฑ์Šค ๊ณ„์‚ฐํ•˜๊ธฐ

 

/// ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„
  private func getInfiniteScrollIndex(newValue: Int) {
    
    if newValue == 0 {
      // ์ฒ˜์Œ์œผ๋กœ ๊ฐ”์„ ๋•Œ ๋์ชฝ์œผ๋กœ ์ด๋™
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        currentIndex = colors.count - 2
      }
    } else if newValue == colors.count - 1 {
      // ๋งˆ์ง€๋ง‰์œผ๋กœ ๊ฐ”์„ ๋•Œ ์ฒซ์ชฝ์œผ๋กœ ์ด๋™
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        currentIndex = 1
      }
    }
    
  }

 

3. ๋ฉ”์ธ ๊ตฌํ˜„ (์ „์ฒด ์ฝ”๋“œ)

 

import SwiftUI

struct CustomCarouselSlides3: View {
  
  /// ์ˆœํ™˜๋ฐฐ์—ด
  @State var colors: [Color] = [.red, .orange, .yellow, .green, .blue]
  /// ํ˜„์žฌ ์ธ๋ฑ์Šค
  @State private var currentIndex: Int = 1
  
  var body: some View {
    TabView(selection: $currentIndex) {
      
      // ์ˆœํ™˜ ๋ฆฌ์ŠคํŠธ์—์„œ ์ฒซ ๋ฒˆ์งธ์™€ ๋งˆ์ง€๋ง‰ ์ƒ‰์„ ์ถ”๊ฐ€
      ForEach(0..<colors.count, id: \.self) { index in
        
        Rectangle()
          .fill(colors[index])
          .frame(width: UIScreen.main.bounds.width)
          .tag(index) // ํ˜„์žฌ ์ธ๋ฑ์Šค๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ํƒœ๊ทธ
        
      } //: ForEach
      .ignoresSafeArea()
      
    } //: Tabview
    .ignoresSafeArea()
    
    // MARK: - ์ธ๋””์ผ€์ดํ„ฐ
    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    /// Custom Indicator
    .overlay(alignment:.bottom) { }
    
    // MARK: - LifeCycle
    .onAppear {
      configureCircularItemList() // ์•ž๋’ค ๋ฐฐ์—ด์— ์ถ”๊ฐ€
      currentIndex = 1
    }
    
    // MARK: - Action
    .onChange(of: currentIndex) { newValue in
      getInfiniteScrollIndex(newValue: newValue) // ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„

    } //: onChange
    
  } //: Body
} //: View

extension CustomCarouselSlides3 {
  
  /// ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„
  private func getInfiniteScrollIndex(newValue: Int) {
    
    if newValue == 0 {
      // ์ฒ˜์Œ์œผ๋กœ ๊ฐ”์„ ๋•Œ ๋์ชฝ์œผ๋กœ ์ด๋™
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        currentIndex = colors.count - 2
      }
    } else if newValue == colors.count - 1 {
      // ๋งˆ์ง€๋ง‰์œผ๋กœ ๊ฐ”์„ ๋•Œ ์ฒซ์ชฝ์œผ๋กœ ์ด๋™
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        currentIndex = 1
      }
    }
    
  }
  
  /// ํŽ˜์ดํฌ ์•„์ดํ…œ ์ถ”๊ฐ€
  private func configureCircularItemList() {
    // ์‹œ์ž‘๊ณผ ๋์— ๋งˆ์ง€๋ง‰, ์ฒซ ๋ฒˆ์งธ ์ƒ‰์ƒ ์ถ”๊ฐ€
    colors.insert(colors[colors.count - 1], at: 0)
    colors.append(colors[1])
  }
  
  /// ์ฐ ์ธ๋ฑ์Šค ๊ตฌํ•˜๊ธฐ
  /// - ์ธ๋””์ผ€์ดํ„ฐ์™€ ํ˜„์žฌ ์ธ๋ฑ์Šค ๋งž์ถ”๊ธฐ ์œ„ํ•จ
  private func getRealIndex() -> Int { }

}

 

OnAppear:

- ์ˆœํ™˜ ๋„๋Š” ๋ฐฐ์—ด์— ์•„์ดํ…œ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ๋ณธ ์ธ๋ฑ์Šค๋Š” 0์ด ์•„๋‹ˆ๋ผ 1๋กœ ์„ค์ •

- ํ™”๋ฉด์— ๋“ค์–ด์˜ฌ ๋•Œ ๋ฐฐ์—ด์— ๊ฐ๊ฐ ๋งˆ์ง€๋ง‰, ์ฒซ๋ฒˆ์งธ ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ (configureCircularItemList)

 

ForEach:

- TabView์˜ selection์œผ๋กœ current Index๋ฅผ ์ถ”์ ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐ ์•„์ดํ…œ์˜ ์ธ๋ฑ์Šค๋Š” tag๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ถ”์ ํ•œ๋‹ค.

 

onChange:

- current Index๊ฐ€ ๋ฐ”๋€” ๋•Œ์˜ ํ•„์š” ์•ก์…˜์„ ์ถ”๊ฐ€ ํ–ˆ๋Š”๋ฐ, ์ธ์œ„์ ์œผ๋กœ ์ถ”๊ฐ€ํ•œ ํŽ˜์ดํฌ ์•„์ดํ…œ์„ ํ†ตํ•ด ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„

- ํ•ด๋‹น ๋ถ€๋ถ„์€ getInfiniteScrollIndex

 

 

 

 

๊ตฌํ˜„ (ํƒ€์ด๋จธ O):

 

 

 

 

1. ์ „์ฒด์ฝ”๋“œ

 

import SwiftUI

struct CustomCarouselSlides3: View {
  
  /// ์ˆœํ™˜๋ฐฐ์—ด
  @State var colors: [Color] = [.red, .orange, .yellow, .green, .blue]
  /// ํƒ€์ด๋จธ
  @State var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
  /// ํ˜„์žฌ ์ธ๋ฑ์Šค
  @State private var currentIndex: Int = 1
  
  var body: some View {
    TabView(selection: $currentIndex) {
      
      // ์ˆœํ™˜ ๋ฆฌ์ŠคํŠธ์—์„œ ์ฒซ ๋ฒˆ์งธ์™€ ๋งˆ์ง€๋ง‰ ์ƒ‰์„ ์ถ”๊ฐ€
      ForEach(0..<colors.count, id: \.self) { index in
        
        Rectangle()
          .fill(colors[index])
          .frame(width: UIScreen.main.bounds.width)
          .tag(index) // ํ˜„์žฌ ์ธ๋ฑ์Šค๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ํƒœ๊ทธ
        
      } //: ForEach
      .ignoresSafeArea()
      
    } //: Tabview
    .ignoresSafeArea()
    
    // MARK: - ์ธ๋””์ผ€์ดํ„ฐ
    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    /// Custom Indicator
    .overlay(alignment:.bottom) { }
    
    // MARK: - LifeCycle
    .onAppear {
      configureCircularItemList() // ์•ž๋’ค ๋ฐฐ์—ด์— ์ถ”๊ฐ€
      currentIndex = 1
    }
    
    // MARK: - Action
    .gesture(
      DragGesture()
        .onChanged { _ in
          timer.upstream.connect().cancel()
          
          DispatchQueue.main.asyncAfter(deadline: .now() + 3){
            timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
          }
        }
    )
    
    .onChange(of: currentIndex) { newValue in
      getInfiniteScrollIndex(newValue: newValue) // ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„

    } //: onChange
    
    .onReceive(timer) { _ in
      /// ํƒ€์ด๋จธ ๊ฐ€๋™ ์‹œ ์ธ๋ฑ์Šค ํ•˜๋‚˜์”ฉ ์˜ฎ๊น€
      withAnimation(.easeIn) {
        currentIndex += 1
      }
    }
    
  } //: Body
} //: View

 

 

2. ๊ธฐ์กด ์ฝ”๋“œ์— ์ถ”๊ฐ€ ๋œ ๋ถ€๋ถ„

 

  /// 1.ํƒ€์ด๋จธ
  @State var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
  
  -------------------------------
  
  /// 2.ํƒ€์ด๋จธ ๊ฐ€๋™
  .onReceive(timer) { _ in
      /// ํƒ€์ด๋จธ ๊ฐ€๋™ ์‹œ ์ธ๋ฑ์Šค ํ•˜๋‚˜์”ฉ ์˜ฎ๊น€
      withAnimation(.easeIn) {
        currentIndex += 1
      }
    }
    
  -------------------------------
  
  /// 3.ํƒ€์ด๋จธ ์ดˆ๊ธฐํ™” + ์žฌ๊ธฐ๋™
  .gesture(
      DragGesture()
        .onChanged { _ in
          timer.upstream.connect().cancel()
          
          DispatchQueue.main.asyncAfter(deadline: .now() + 3){
            timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
          }
        }
    )

 

1. 3์ดˆ ํƒ€์ด๋จธ ์ถ”๊ฐ€

 

2. onReceive

- ํƒ€์ด๋จธ๊ฐ€ ๋ณ€๊ฒฝ ๋  ๋•Œ๋งˆ๋‹ค (3์ดˆ๋งˆ๋‹ค) ์ธ๋ฑ์Šค๋ฅผ ๋ณ€๊ฒฝ

- withAnimation์„ ์ถ”๊ฐ€ํ•ด์„œ ์Šฌ๋ผ์ด๋“œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌํ˜„

 

3. gesture()๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ํ™”๋ฉด ์ด๋™ ์‹œ ํƒ€์ด๋จธ ๊ด€๋ฆฌ

...๋ฅผ ์•ˆํ•ด์ฃผ๋ฉด ๋“œ๋ž˜๊ทธ๋ฅผ ํ•˜๋Š” ์™€์ค‘์— 3์ดˆ๊ฐ€ ์ง€๋‚˜๋ฉด ์ธ๋ฑ์Šค๊ฐ€ +1์ด ๋˜์–ด๋ฒ„๋ฆฐ๋‹ค.

- ์‚ฌ์šฉ์ž์˜ ๋“œ๋ž˜๊ทธ ์•ก์…˜์ด ๊ฐ์ง€ ๋˜๋ฉด ํƒ€์ด๋จธ๋ฅผ ์ดˆ๊ธฐํ™”

- 3์ดˆ ๋’ค ํƒ€์ด๋จธ ๊ฐ€๋™

 

 

 

 

 

 

 

 

 

 

References

 

https://ios-development.tistory.com/1197

 

[iOS - swift] Infinite Carousel (๋ฌดํ•œ ์Šคํฌ๋กค ๋ทฐ) ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

๊ตฌํ˜„ ์•„์ด๋””์–ด ์ˆ˜ํ‰ ์Šคํฌ๋กค์„ ์œ„ํ•ด์„œ UIScrollView๋ฅผ ์ด์šฉํ•ด๋„ ๋˜์ง€๋งŒ, ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ž…๋ ฅ ํŽธ์˜๋ฅผ ์œ„ํ•ด UICollectionView ์‚ฌ์šฉ ๋ฌดํ•œ ์Šคํฌ๋กค ์›๋ฆฌ (๋ฐ์ดํ„ฐ๊ฐ€ 1,2,3 ์ด๋ ‡๊ฒŒ ์žˆ์„ ๊ฒฝ์šฐ,) ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ

ios-development.tistory.com

 

 

 

 


 

์‚ฌ์‹ค ์ด๊ฒŒ ์ตœ์„ ์˜ ๋ฐฉ๋ฒ•์ธ์ง€๋Š” ๋ชจ๋ฅด๊ฒ ๋‹ค...

๋‚˜์ค‘์— ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์„ ์•Œ๊ฒŒ ๋˜๋ฉด ์‹œ๋ฆฌ์ฆˆ์ฒ˜๋Ÿผ ๋‹ค์‹œ ๊ฒŒ์‹œ๊ธ€์„ ์“ฐ๊ฑฐ๋‚˜ ์ด๊ณณ์— ์ถ”๊ฐ€ํ•ด์•ผ๊ฒ ๋‹ค.

์ €์ž‘์žํ‘œ์‹œ (์ƒˆ์ฐฝ์—ด๋ฆผ)

'๐ŸŽ Dev > ๊ตฌํ˜„' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[SwiftUI] Infinite Carousel ๊ตฌํ˜„ํ•˜๊ธฐ 1 (feat. Timer)  (0) 2024.09.05
[SwiftUI] pagerView ๋งŒ๋“ค๊ธฐ (iOS ๋ฒ„์ „๋Œ€์‘)  (2) 2024.08.14
[SwiftUI] CustomPopUpView ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ ํ•ด๊ฒฐํ•˜๊ธฐ  (0) 2024.06.27
[SwiftUI] ์Šค์œ ๋กœ ์„น์…˜ ์ ‘์—ˆ๋‹คํˆ๋‹ค ๊ตฌํ˜„ํ•˜๊ธฐ  (0) 2024.06.21
[SwiftUI] pre/next buttons๊ฐ€ ์žˆ๋Š” ์ด๋ฏธ์ง€ ์Šฌ๋ผ์ด๋” ๊ตฌํ˜„ํ•˜๊ธฐ  (2) 2024.05.16
'๐ŸŽ Dev/๊ตฌํ˜„' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • [SwiftUI] Infinite Carousel ๊ตฌํ˜„ํ•˜๊ธฐ 1 (feat. Timer)
  • [SwiftUI] pagerView ๋งŒ๋“ค๊ธฐ (iOS ๋ฒ„์ „๋Œ€์‘)
  • [SwiftUI] CustomPopUpView ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ ํ•ด๊ฒฐํ•˜๊ธฐ
  • [SwiftUI] ์Šค์œ ๋กœ ์„น์…˜ ์ ‘์—ˆ๋‹คํˆ๋‹ค ๊ตฌํ˜„ํ•˜๊ธฐ
Callie_
Callie_
  • Callie_
    CalliOS
    Callie_
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ
      • ๐ŸŽ APPLE
      • ๐ŸŽ Dev
        • Swift
        • UIKit
        • SwiftUI
        • Issue
        • ๊ตฌํ˜„
      • ๐ŸŽ Design
        • HIG
      • โš™๏ธ CS
      • ๐Ÿ’ก ์•Œ๊ณ ๋ฆฌ์ฆ˜
        • ํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค
        • ๋ฐฑ์ค€
      • ๐ŸŸ๏ธ ์ง๊ด€๋กœ๊ทธ (์ถœ์‹œ์•ฑ)
        • ์—…๋ฐ์ดํŠธ
      • ๐ŸŒฑ SeSAC iOS 3๊ธฐ
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ํ™ˆ
    • ํƒœ๊ทธ
  • ๋งํฌ

  • ๊ณต์ง€์‚ฌํ•ญ

  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    ios
    Snapshot
    ๋„คํŠธ์›Œํฌํ†ต์‹ 
    IBOutlet
    Swift
    CustomView
    .fullScreen
    assets
    Infoํƒญ
    DidEndOnExit
    diffable
    stroyboard
    clipsToBound
    DiffableDataSource
    cornerradius
    SeSAC
    layer.shadow
    IBAction
    ์ƒ๋ช…์ฃผ๊ธฐ
    Entry Point
    .OverFullScreen
    modalPresentStyle
    TableViewCell
    Enum
    apply
    ํ™”๋ฉด์ „ํ™˜
    SwiftUI
    CocoaTouchFramework
    addTarget
    TapGestureRecognizer
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.0
Callie_
[SwiftUI] Infinite Carousel ๊ตฌํ˜„ํ•˜๊ธฐ 2 (feat.Timer)
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”