Sheet freeze in SwiftUI when parent-View gets updated - swiftui

Using Swift5.3.2, iOS14.4.1, XCode12.4,
I am showing a modal sheet in SwiftUI.
Unfortunately, the sheet completely freezes whenever an underlying parent-View re-renders/updates.
I am absolutely clueless on how I can circumvent that issue. Any ideas ?
Here is the parent View:
import SwiftUI
struct ParentTrialView: View {
#State var currentDate = Date()
#State private var showingGrid = false
let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
Text("\(currentDate)")
.onReceive(timer) { input in
currentDate = input
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showingGrid.toggle()
}) {
Image(systemName: "square.grid.3x3")
}
.sheet(isPresented: $showingGrid) {
ChildTrialView(onDismiss: {
showingGrid = false
})
}
}
}
}
}
}
And here is the child View:
import SwiftUI
struct ChildTrialView: View {
var onDismiss: () -> ()
var body: some View {
NavigationView {
Text("Trial")
.onTapGesture {
onDismiss()
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
onDismiss()
}) {
Text("Cancel")
}
}
}
}
}
}
If you, for example, set the timer to 20 seconds. Then you have 20 seconds where everything works on the child View (i.e. it's tap-Gesture works, and also its Cancel button to close the child-View works perfectly).
However, when the timer fires an update to the parent-View, then no action works on the child-View anymore !!
I am more than desperate with this SwiftUI problem.
Any solution on this ?

Related

SwiftUI: Logout and switch screens from a popover

In my app, I have a View which is set to either a login View or a home TabView, depending on if the user is logged in. From the TabView, the user can go to a profile popover and logout. I want to switch back to the login View from this popover.
I tried dismissing the popover and immediately logging the user out, but when I test on a real device, what happens is the popover stays on the screen and also no longer responds to user input. It can't be dismissed. I'm not sure why, and what should I do instead?
Starting View:
struct StartView: View {
#EnvironmentObject var authService:AuthService
var body: some View {
ZStack {
if(!authService.signedIn) {
LoginView()
} else {
HomeView()
}
}
}
}
Home TabView:
import SwiftUI
struct HomeView: View {
#State private var showingProfilePopover:Bool = false
var body: some View {
TabView {
NavigationView {
VStack(alignment: .leading) {
Text("Tab 1")
.padding(.leading, 30)
}
.toolbar {
ToolbarItem {
Button(action: {
showingProfilePopover = true
}, label: {
Image(systemName: "person.crop.circle").imageScale(.large)
}
)
}
}
}.popover(isPresented: $showingProfilePopover) {
ProfileView(isPresented: $showingProfilePopover)
}
.tabItem {
Image(systemName: "list.bullet")
.font(.system(size: 26))
Text("Tab 1")
}
NavigationView {
VStack(alignment: .leading) {
Text("Tab 2")
}
}.tabItem {
Image(systemName: "books.vertical")
.font(.system(size: 26))
Text("Tab 2")
}
}
}
}
Popover:
struct ProfileView: View {
#EnvironmentObject var authService:AuthService
#Binding var isPresented: Bool
var body: some View {
Button("Logout") {
// Close the popup and switch to LoginView
print("Tapped logout")
isPresented = false
authService.signOut()
}
.font(Font.custom("OpenSans-Regular", size: 18))
.padding(20)
}
}
LoginView:
import SwiftUI
struct LoginView: View {
#EnvironmentObject var authService:AuthService
var body: some View {
VStack {
Button("Login") {
self.authService.signIn()
}.buttonStyle(.borderedProminent)
}
}
}
AuthService:
import SwiftUI
class AuthService: ObservableObject {
#Published var signedIn:Bool
init(signedIn:Bool) {
self.signedIn = signedIn
}
func signIn() {
self.signedIn = true
}
func signOut(){
self.signedIn = false
}
}
Seems like an issue connected to .popover. I can reproduce the issue, but it works just fine using .sheet instead.
Consider attaching the .popover on the TabView or the Button itself then it seems to work just fine.
I realized the issue only happens on the older versions of iOS. it works fine on iOS 15, but not on iOS 14 and below iOS 14.
#main
struct LoginApp: App {
let authService: AuthService
init() {
authService = AuthService(signedIn: false)
}
var body: some Scene {
WindowGroup {
StartView().environmentObject(authService)
}
}
}
This is iPhone 8, iOS 14

SwiftUI sharing timer through views in navigation view

I'm pretty new at SwiftUI.
I would like to make an app that navigates through different views in a Navigation View and running a timer in background presenting the time on each of the views.
The problem is when I navigate to a second level of navigation. When the timer is on, the app returns automatically to the previous navigation view.
Here's is a screenshot video of what I mean:
https://youtu.be/eXbK9jpluvk
Here is my code:
import SwiftUI
struct ContentView: View {
#State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var seconds : Int = 0
#State var paused : Bool = true
var body: some View {
VStack {
Text("\(seconds)")
Button(action: {
paused.toggle()
}) {
Image(systemName: paused ? "play.fill" : "stop.fill")
}
NavigationLink(destination: FirstView( timerSeconds: $seconds, timerPaused: $paused)) {
Text("Navigate 1")
}.padding()
}
.onReceive(self.timer) { currentTime in
if !paused {
seconds = seconds + 1
print(currentTime)
}
}
.navigationViewStyle(.stack)
}
}
struct FirstView: View {
#Binding var timerSeconds : Int
#Binding var timerPaused : Bool
var body: some View {
VStack {
Text("First View")
Text("\(timerSeconds)")
Button(action: {
timerPaused.toggle()
}) {
Image(systemName: timerPaused ? "play.fill" : "stop.fill")
}
NavigationLink(destination: SecondView(seconds: $timerSeconds)) {
Text("Navigate 2")
}
}
}
}
struct SecondView: View {
#Binding var seconds : Int
var body: some View {
Text("\(seconds)")
}
}
Any help will be welcome!!
Thanks a lot
NavigationView can only push on one detail NavigationLink so to have more levels you need to set .isDetailLink(false) on the link. Alternatively, if you don't expect to run in landscape split view, you could set .navigationViewStyle(.stack) on the navigation.

unexpected behaviour with SwiftUI .constant(nil) binding in SheetView

I came across some issues with my SwiftUI code.
I made a simple example.
Just a Button that opens a Sheet.
struct ContentView: View {
#State private var showSheet = false
var body: some View {
Button(action: {
showSheet.toggle()
}, label: {
Text("Button")
})
.sheet(isPresented: $showSheet) {
SheetView(show: $showSheet, selectedDate: .constant(nil))
}
}
}
The Sheet has an optional Binding.
You can close it via the mark button.
However, as soon as I use an #Environment wrapper, the xmark stops working.
struct SheetView: View {
#Binding var show: Bool
#Environment(\.colorScheme) var colorScheme
#Binding var selectedDate: Date?
var body: some View {
NavigationView {
Text("Hello, \(selectedDate?.description ?? "Welt")!")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.show = false
}) {
Image(systemName: "xmark")
.renderingMode(.original)
.accessibilityLabel(Text("Save"))
}
}
}
}
}
}
(This seems to be due to the view constantly refreshing itself?)
In addition: This only happens on my iPhone 12 Pro Max.
In the simulator or an iPhone 11 Pro Max it is working fine.
(Don't have any other devices to test on.)
If I don't use .constant(nil) but give it a $Date, it works just fine.
If I remove the #Environment, it also works.
Am I doing something wrong here?
What is my mistake?

SwiftUI NavigationView with PresentationMode creates bug in multilevel navigation hierarchy

I have an iOS 13.5 SwiftUI (macOS 10.15.6) app that requires the user to navigate two levels deep in a NavigationView hierarchy to play a game. The game is timed. I'd like to use custom back buttons in both levels, but if I do, the timer in the second level breaks in a strange way. If I give up on custom back buttons in the first level and use the system back button everything works. Here is a minimum app that replicates the problem:
class SimpleTimerManager: ObservableObject {
#Published var elapsedSeconds: Double = 0.0
private(set) var timer = Timer()
func start() {
print("timer started")
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) {_ in
if (Int(self.elapsedSeconds * 100) % 100 == 0) { print ("\(self.elapsedSeconds)") }
self.elapsedSeconds += 0.01
}
}
func stop() {
timer.invalidate()
elapsedSeconds = 0.0
print("timer stopped")
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: CountDownIntervalPassThroughView()) {
Text("Start the timer!")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CountDownIntervalPassThroughView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
VStack {
NavigationLink(destination: CountDownIntervalView()) {
Text("One more click...")
}
Button(action: {
self.mode.wrappedValue.dismiss()
print("Going back from CountDownIntervalPassThroughView")
}) {
Text("Go back!")
}
}
.navigationBarBackButtonHidden(true)
}
}
struct CountDownIntervalView: View {
#ObservedObject var timerManager = SimpleTimerManager()
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var interval: Double { 10.0 - self.timerManager.elapsedSeconds }
var body: some View {
VStack {
Text("Time remaining: \(String(format: "%.2f", interval))")
.onReceive(timerManager.$elapsedSeconds) { _ in
print("\(self.interval)")
if self.interval <= 0 {
print("timer auto stop")
self.timerManager.stop()
self.mode.wrappedValue.dismiss()
}
}
Button(action: {
print("timer manual stop")
self.timerManager.stop()
self.mode.wrappedValue.dismiss()
}) {
Text("Quit early!")
}
}
.onAppear(perform: {
self.timerManager.start()
})
.navigationBarBackButtonHidden(true)
}
}
Actually, with this example code, there is some strange behavior even if I use the system back, although my full app doesn't have that problem and I can't see any differences that would explain why. But - for the moment I'd like to focus on why this code breaks with multi-level custom back buttons.
Thanks in advance for any thoughts on this...

Using SwiftUI, we installed a Button in the List. Why does the modal disappear when I tap the button to display the modal and then close it again?

I am now learning to create a sample code for SwiftUI using the official version of Xcode11.
I wrote a simple code to show and hide modal.
This code adds a button to the list and displays a modal.
Strangely, however, the modal no longer appears when the button is tapped again after closing.
Is there a reason for this or any solution?
Occurs when there is a button in the list, but if you delete only the list from the code, the modal can be displayed as many times as you like.
This is the code that causes the bug.
struct ContentView: View {
#State var show_modal = false
var body: some View {
List {
Button(action: {
print("Button Pushed")
self.show_modal = true
}) {
Text("Show Modal")
}.sheet(isPresented: self.$show_modal, onDismiss: {
print("dismiss")
}) {
ModalView()
}
}
}
}
This is a code that does not cause a bug.
struct ContentView: View {
#State var show_modal = false
var body: some View {
Button(action: {
print("Button Pushed")
self.show_modal = true
}) {
Text("Show Modal")
}.sheet(isPresented: self.$show_modal, onDismiss: {
print("dismiss")
}) {
ModalView()
}
}
}
The only difference is whether or not there is a List.
The ModalView code is below.
struct ModalView: View {
// 1. Add the environment variable
#Environment(\.presentationMode) var presentationMode
var body: some View {
// 2. Embed Text in a VStack
VStack {
// 3. Add a button with the following action
Button(action: {
print("dismisses form")
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Dismiss")
}.padding(.bottom, 50)
Text("This is a modal")
}
}
}
When breakpoint is set, print ("Button Pushed") is called every time, but ModalView of .sheet is not called, and naturally the body of ModalView class is not called.
I think the issue is that your .sheet is not on the List itself but on the Button in your code that causes the bug.
Try this instead:
struct ContentView: View {
#State var show_modal = false
var body: some View {
List {
Button(action: {
print("Button Pushed")
self.show_modal = true
}) {
Text("Show Modal")
}
}.sheet(isPresented: self.$show_modal, onDismiss: {
print("dismiss")
}) {
ModalView()
}
}
}