'Change a property on TabView drag gesture in SwiftUI (View pager)

I have written a generic ViewPager with TabView and it works perfectly. However, I want to pause the timer (auto swipe) when user starts dragging and resume it when user finishes the dragging. Is there anyway to do that?

This is my ViewPager:

struct ViewPager<Data, Content> : View
where Data : RandomAccessCollection, Data.Element : Identifiable, Content : View {

private var timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()

@Binding var currentIndex: Int

private let data: [Data.Element]
private let content: (Data.Element) -> Content
private let isTimerEnabled: Bool
private let showIndicator: PageTabViewStyle.IndexDisplayMode

init(_ data: Data,
     currentIndex: Binding<Int>,
     isTimerEnabled: Bool = false,
     showIndicator: PageTabViewStyle.IndexDisplayMode = .never,
     @ViewBuilder content: @escaping (Data.Element) -> Content) {
    
    _currentIndex = currentIndex
    self.data = data.map { $0 }
    self.content = content
    self.isTimerEnabled = isTimerEnabled
    self.showIndicator = showIndicator
}

private var totalCount: Int {
    data.count
}

var body: some View {
    TabView(selection: $currentIndex) {
        ForEach(data) { item in
            self.content(item)
                .tag(item.id)
        }
    }.tabViewStyle(PageTabViewStyle(indexDisplayMode: showIndicator))
        .onReceive(timer) { _ in
            if !isTimerEnabled {
                timer.upstream.connect().cancel()
            } else {
                withAnimation {
                    currentIndex = currentIndex < (totalCount - 1) ? currentIndex + 1 : 0
                }
            }
            
        }
    }
}


Solution 1:[1]

To "pause" while user is dragging, you can exchange .common with .default in the Timer. But what you probably also want is setting the timer back to 2 secs once the dragging is over ...

I got this to work but I use a global var, so the timer stays around and this feels wrong – can someone help further?

// global var – this seems wrong, but works
var timer = Timer.publish(every: 2, on: .main, in: .default).autoconnect()


struct ViewPager<Data, Content> : View
where Data : RandomAccessCollection, Data.Element : Identifiable, Content : View {
    
    
    @Binding var currentIndex: Int
    
    private let data: [Data.Element]
    private let content: (Data.Element) -> Content
    private let isTimerEnabled: Bool
    private let showIndicator: PageTabViewStyle.IndexDisplayMode
    
    init(_ data: Data,
         currentIndex: Binding<Int>,
         isTimerEnabled: Bool = false,
         showIndicator: PageTabViewStyle.IndexDisplayMode = .never,
         @ViewBuilder content: @escaping (Data.Element) -> Content) {
        
        _currentIndex = currentIndex
        self.data = data.map { $0 }
        self.content = content
        self.isTimerEnabled = isTimerEnabled
        self.showIndicator = showIndicator
    }
    
    private var totalCount: Int {
        data.count
    }
    
    var body: some View {
        TabView(selection: $currentIndex) {
            ForEach(data) { item in
                self.content(item)
                    .tag(item.id)
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: showIndicator))
        
        .onReceive(timer) { _ in
            if !isTimerEnabled {
                timer.upstream.connect().cancel()
            } else {
                print("received")
                withAnimation {
                    currentIndex = currentIndex < (totalCount - 1) ? currentIndex + 1 : 0
                    print(currentIndex)
                }
            }
        }
        
        .onChange(of: currentIndex) { _ in
            timer = Timer.publish(every: 2, on: .main, in: .default).autoconnect()
        }
        
    }
}

Solution 2:[2]

same principle but used selection for TabView (and tag for view) and timer as not global var

struct MotivationTabView: View {
// MARK: - PROPERTIES
@State private var selectedItem = "Adolf Dobr’a?sk?j"
@State private var isTimerEnabled: Bool = true
@State private var timer = Timer.publish(every: 10, on: .main, in: .default).autoconnect()

let items: KeyValuePairs = ["Adolf Dobr’a?sk?j": "Svij narod treba ?ubyty i ne ha?byty s’a za ?oho!",
                            "Fjodor Mychailovi? Dostojevsk?j": "Chto ne maje narod, tot ne maje any Boha! Bu?te sobi ist?, že vš?tk? tot?, što perestanu? rozumity svomu narodu i stra?aju? z nym perevjaza?a, stra?aju? jedno?asno viru otc’ovsku, stavaju? bu? ateistamy, abo cholodn?ma.",
                            "Pau del Rosso": "Je barz važn?m uchovaty sobi vlastnu identi?nos?. To naš unikatn?j dar pro druh?ch, unikatn?j v cilim kozmosi.",
                            "Viktor Hugo": "Velykos? naroda ne mir’a? s’a ki?kos?ov, tak jak i velykos? ?olovika ne mir’a? s’a v?škov.",
                            "Lewis Lapham":"Strata identit? je v?hoda pro biznis... pok?a b? jem znav, chto jem, ?om b? jem si bezprestajno kupovav nov? zna?k? vod? po holi?u?"]

private var totalCount: Int {
    items.count
}

private func nextItem(currItem: String) -> String{
    switch currItem {
    case "Adolf Dobr’a?sk?j": return "Fjodor Mychailovi? Dostojevsk?j"
    case "Fjodor Mychailovi? Dostojevsk?j": return "Pau del Rosso"
    case "Pau del Rosso": return "Viktor Hugo"
    case "Viktor Hugo": return "Lewis Lapham"
    default: return  "Adolf Dobr’a?sk?j"
    }
}


// MARK: - BODY
var body: some View {
    GroupBox {
        TabView(selection: $selectedItem){
            ForEach(items, id: \.self.key) { item in
                VStack(alignment: .trailing){
                    Text(item.value)
                    Text(item.key)
                        .font(.caption)
                        .padding(.top, 5)
                    
                }.tag(item.key)
            }
        } //: TABS
        .tabViewStyle(PageTabViewStyle())
        .frame(height: 240)
        .onReceive(timer) { _ in
            if !isTimerEnabled {
                timer.upstream.connect().cancel()
            } else {
                print("received")
                withAnimation() {
                    selectedItem = nextItem(currItem: selectedItem)
                    print(selectedItem)
                }
            }
        }
        .onAppear{
            isTimerEnabled = true
            timer = Timer.publish(every: 10, on: .main, in: .default).autoconnect()
        }
        .onDisappear{
            isTimerEnabled = false
        }
    } //: BOX
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ChrisR
Solution 2 Peter