NSTimer interfering with user clicks

47 Views Asked by At

I am trying to update a SwiftUI Image very frequently. That image view is clickable and gets a highlight if selected.

When using an NSTimer with a short interval (0.25 seconds) for the image update, my SwiftUI view does not respond properly to user clicks anymore - clicks are only caught intermittently. If I set the timer interval to 1 second, things would work fine, however, that's not possible in my specific situation.

How can I ensure that my SwiftUI Image's onTapGesture works smoothly even with a high timer frequency?

The timer is declared as such:

let timer = Timer(timeInterval: 0.5, repeats: true, block: { [weak self] timer in
            guard let strongSelf = self else {
                timer.invalidate()
                return
            }

            // updating an observable object here which will be propagated to the ScreenElement view below
        })
        timer.tolerance = 0.2

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

Then I have the SwiftUI view declared as such:

struct ScreenElement: View {
    var body: some View {
        VStack(alignment: .center, spacing: 12)
        {
            Image(nsImage: screen.imageData)
                .resizable()
                .aspectRatio(174/105, contentMode: .fit)
                .background(Asset.gray900.swiftUIColor)
                .cornerRadius(12)

            Text(screen.name)
        }
        .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
        .onTapGesture {
            // modify data source and mark this current element as the highlighted (selected) one
        }
    }
}

What I have tried:

I tried to move the timer to a background thread which didn't really work and/or caused more problems than it solved. Additionally, I tried to increase the timer interval, which, however, is not feasible in my use case since it has to be a very high refresh rate.

Some further considerations I had but couldn't answer:

Is it maybe possible that SwiftUI just doesn't support a frequent refresh of 4x per second? Or did I maybe use the wrong UI Element to handle the tap gesture in my particular case? Or is it just not possible to have a timer with such frequent updates since it overloads the main thread?

Any help will be greatly appreciated!

1

There are 1 best solutions below

2
Ashley Mills On

The following works fine on an iPhone and Mac, all taps are recognised and it's updating at 10 times per second:

struct ContentView: View {

    let imageNames = ["eraser", "lasso.and.sparkles", "folder.badge.minus", "externaldrive.badge.xmark", "calendar.badge.plus", "arrowshape.zigzag.forward", "newspaper.circle", "shareplay", "person.crop.square.filled.and.at.rectangle.fill"]
    
    @State private var currentImageNames: [String] = ["", "", ""]
    @State private var selected: Int?

    var body: some View {
        VStack {
            HStack {
                ForEach(0..<3, id: \.self) { index in
                        Image(systemName: currentImageNames[index])
                            .imageScale(.large)
                            .frame(width: 80, height: 60)
                            .contentShape(Rectangle())
                            .onTapGesture {
                                selected = index
                            }
                            .background(
                                Rectangle()
                                    .fill(selected == index ? .red : .blue)
                            )
                }
            }
        }
        .onAppear {
            addTimer()
        }
        .padding()
    }
    
    func addTimer() {
        let timer = Timer(timeInterval: 0.1, repeats: true) { timer in
            currentImageNames = [imageNames.randomElement()!, imageNames.randomElement()!, imageNames.randomElement()!]
        }
        RunLoop.current.add(timer, forMode: .common)
    }
}

Is there something else you're possibly doing that is blocking the main queue?