Kevin R
Kevin R

Reputation: 8621

SwiftUI stops updates during scrolling of List

Given a List in SwiftUI, once panning begins, updating of views in the list seems to pause until the scrolling has been stopped. Is there a way to prevent this?

Consider the following code:

class Model: ObservableObject, Identifiable {
    @Published var offset: CGFloat = 0

    let id = UUID()
    private var timer: Timer!

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
            self.update()
        })
    }

    func update() {
        offset = CGFloat.random(in: 0...300)
    }
}

struct ContentView: View {
    @ObservedObject var model1 = Model()
    @ObservedObject var model2 = Model()
    @ObservedObject var model3 = Model()
    @ObservedObject var model4 = Model()

    var body: some View {
        List {
            ForEach([model1, model2, model3, model4]) {
                Rectangle()
                    .foregroundColor(.red)
                    .frame(width: $0.offset, height: 30, alignment: .center)
                    .animation(.default)
            }
        }
    }
}

Will result in this behaviour:

Video of result

Upvotes: 9

Views: 1526

Answers (2)

rob mayoff
rob mayoff

Reputation: 385500

You could use GCD as in Asperi's answer, but that doesn't explain why your code didn't work.

The problem is that, while the scroll view is tracking your touch, it runs the run loop in the .tracking mode. But because you created your Timer using scheduledTimer(withTimeInterval:repeats:block:), the Timer is only set to run in the .default mode.

You could add the timer to all the common run loop modes (which include .tracking) like this:

RunLoop.main.add(timer, forMode: .common)

But I would probably use a Combine publisher instead, like this:

class Model: ObservableObject, Identifiable {
    @Published var offset: CGFloat = 0

    let id = UUID()

    private var tickets: [AnyCancellable] = []

    init() {
        Timer.publish(every: 0.5, on: RunLoop.main, in: .common)
            .autoconnect()
            .map { _ in CGFloat.random(in: 0...300) }
            .sink { [weak self] in self?.offset = $0 }
            .store(in: &tickets)
    }
}

Upvotes: 18

Asperi
Asperi

Reputation: 257493

This due to nature of Timer and RunLoop. Use instead GCD, like in below approach

demo

init() {
    var runner: (() -> ())?
    runner = {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            if let self = self {
                self.update()
                runner?()
            }
        }
    }
    runner?()
}

Upvotes: 7

Related Questions