Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Navigationbar- / Toolbar Button not working reliable when @State variable refresh the view with a high frequency

I noticed that Navigationbar- / Toolbar Buttons are not working properly when there is at least one @State variable refreshing the View very often.

I created a simple app to test it.

With below Code example you have 3 options to trigger a modal sheet. One Button in the main View, one in the toolbar and one in the navigationbar.

When my timer doesn't update "number" all 3 buttons are working properly. When i start the Timer which will refresh the view every 0,1 second only the button in the main view will work every time. The buttons in toolbar / navigationbar do not work most of the time. (The shorter the TimeInterval of my timer the less the buttons are working)

import SwiftUI

struct ContentView: View {

    @State private var number = 0
    @State private var showModal = false

    func startTimer() {
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (_) in
            number += 1
        }
    }

    var body: some View {

        NavigationView {
            VStack {
                Text("Count \(number)")
                Button(action: startTimer, label: {
                    Text("Start Timer")
                })

                Button(action: { showModal.toggle() }, label: {
                    Text("Open Modal")
                })
            }
            .navigationBarTitle("Home", displayMode: .inline)
            .navigationBarItems(leading:
                                    Button(action: {
                                        showModal.toggle()
                                    }, label: {
                                        Text("Open Modal")
                                    }))

            .toolbar {
                ToolbarItem(placement: .bottomBar) {
                    Button(action: {
                        showModal.toggle()
                    }, label: {
                        Text("Open Modal")
                    })
                }
            }
        }

        .sheet(isPresented: $showModal, content: {
            Text("Hello, World!")
        })

    }
}

Does anyone have an idea if there is a way to make the 2 buttons work properly?

This issue occurs with Xcode 12 beta 5 and Xcode Xcode 11.6 (without toolbar as it's not available there)

like image 828
Volker88 Avatar asked Aug 22 '20 20:08

Volker88


2 Answers

The number state in provided variant refreshes entire view, that is a result of issue, and not very optimal for UI.

The solution for navigation bar, and in general good style, is to separate refreshing part into standalone view, so SwiftUI rendering engine rebuild only it.

Tested with Xcode 12b3 / iOS 14

struct TestOftenUpdate: View {

    @State private var showModal = false

    var body: some View {

        NavigationView {
            VStack {
                QuickTimerView()
                Button(action: { showModal.toggle() }, label: {
                    Text("Open Modal")
                })
            }
            .navigationBarTitle("Home", displayMode: .inline)
            .navigationBarItems(leading:
                                    Button(action: {
                                        showModal.toggle()
                                    }, label: {
                                        Text("Open Modal")
                                    }))

            .toolbar {
                ToolbarItem(placement: .bottomBar) {
                    Button(action: {
                        showModal.toggle()
                    }, label: {
                        Text("Open Modal")
                    })
                }
            }
        }

        .sheet(isPresented: $showModal, content: {
            Text("Hello, World!")
        })

    }
}

struct QuickTimerView: View {
    @State private var number = 0

    var body: some View {
        VStack {
            Text("Count \(number)")
            Button(action: startTimer, label: {
                Text("Start Timer")
            })
        }
    }

    func startTimer() {
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (_) in
            number += 1
        }
    }

}

With toolbar is different issue - it does not work after first sheet open even without timer view, and here is why

demo

as it is seen the button layout has corrupted and outside of toolbar area, that is why hit testing fails - and this is Apple defect that should be reported.

Workaround 1:

Place toolbar outside navigation view (visually toolbar is smaller, but sometimes might be appropriate, because button works)

    NavigationView {
       // ... other code
    }
    .toolbar {
        ToolbarItem(placement: .bottomBar) {
            Button(action: {
                showModal.toggle()
            }, label: {
                Text("Open Modal")
            })
        }
    }
like image 191
Asperi Avatar answered Nov 01 '22 22:11

Asperi


Xcode 13.1, iPhone 13 simulator target (and canvas preview).

None of the other solutions worked for me, but I did discover that this only affects top navigation buttons AND if there is a large nav bar title.

So the things that worked seem to be:

  1. Setting an inline title
    .navigationBarTitleDisplayMode(.inline)
    
  2. No title
  3. No top edge toolbar (bottom bar only)

If the problem did manifest, I could pull down on the list contained within the view (no need for refreshable to be enabled) and this would also restore the button functionality.

like image 4
Dave Meehan Avatar answered Nov 01 '22 22:11

Dave Meehan