SwiftUI NavBar button not working while timer updates view - swiftui

Seems there's an issue triggering the NavBar button while timer updates the view.
In the example below, the Close button works, but the NavBar button Cancel, doesn't.
Any idea why? Or how to fix
struct ContentView: View {
#State var isPresented = false
var body: some View {
Button(action: {
isPresented = true
}, label: {
Text("Button")
})
.sheet(isPresented: $isPresented, content: {
ModalView()
})
}
}
class Model: ObservableObject {
private var monitorTimer: Timer?
#Published var someThing: Bool = false
init() {
startMonitoring()
}
func startMonitoring() {
monitorTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { _ in
self.someThing.toggle()
})
}
}
struct ModalView: View {
#Environment(\.presentationMode) private var presentationMode
#StateObject var model = Model()
var body: some View {
NavigationView {
VStack{
if model.someThing {
Text("True")
} else {
Text("False")
}
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}
.navigationBarTitle("Modal")
.navigationBarItems(
leading:
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
})
}
}
}

Here is tested workaround (Xcode 12 / iOS 14). The solution is to separate subviews to avoid global refresh, like below
Note: by the way - don't forget to stop timer on close modal, because state object remains alive, so now your timer runs even after sheet get closed
struct ModalView: View {
#StateObject var model = Model()
var body: some View {
InternalModal().environmentObject(model)
}
}
struct InternalModal: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
NavigationView {
VStack{
UpdatingView()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}
.navigationBarTitle("Modal")
.navigationBarItems(
leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
})
}
}
}
struct UpdatingView: View {
#EnvironmentObject var model: Model
var body: some View {
if model.someThing {
Text("True")
} else {
Text("False")
}
}
}

Related

SwiftUI Half-swipe back from navigation causes error

I noticed issue in SwiftUI when using NavigationStack
Once I swipe-back on a half and revert it -> it stops working
Also I attached sample code if you want to try it
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
var body: some View {
List {
NavigationLink(destination: ViewA(viewModel: .init()), label: {
Text("A")
})
NavigationLink(destination: ViewB(), label: {
Text("B")
})
}
}
}
struct ViewA: View {
#StateObject var viewModel: Observed
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
}, label: {
Text("label")
})
}
NavigationLink(isActive: $viewModel.shouldShowViewB, destination: {
ViewB()
}, label: {EmptyView()})
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}
Expected: whatever I do it should work as expected - when I tap it should open new view
Anyone else found this issue? How to fix it?
Issue 1 is you create the ObservedObject inside the NavigationLink with .init and then have a #StateObject declaration in the Subview ViewA(). That doesn't feel right. Create the Object with #StateObject in the parent view and pass it down.
Issue 2 is the new SwiftUI Navigation model, with NavigationLink)destination: label:) being deprecated. I adapted your code to the new navigation logic:
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
#StateObject var viewModel = Observed() // create ObservedObject here
var body: some View {
List {
NavigationLink("A") {
ViewA(viewModel: viewModel) // pass down
}
NavigationLink("B") {
ViewB()
}
}
}
}
struct ViewA: View {
#ObservedObject var viewModel: Observed // passed down Object
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
print(viewModel.shouldShowViewB)
}, label: {
Text("label")
})
}
.navigationDestination(isPresented: $viewModel.shouldShowViewB, destination: { ViewB() })
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}

Programmatically Presenting and Dismissing Views SwiftUI

I am working on a project that is attempting to present and dismiss views in a NavigationView using state and binding. The reason I am doing this is there is a bug in the #Environment(.presentationMode) var presentaionMode: Binding
model. It's causing odd behavior. It's discussed in this post here.
The example below has three views that are progressively loaded on to the view. The first two ContentView to NavView1 present and dismiss perfectly. However, once NavView2 is loaded, the button that is used to toggle the state of presentNavView2 ends up adding another NavView2 view on the stack and does not dismiss it as expected. Any thoughts as to why this would be?
ContentView
struct ContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(presentNavView1: self.$presentNavView1), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
}) // Button
}) // NavigationLink
} // List
.navigationTitle("Home")
} // NavigationView
} // View
}
NavView1
struct NavView1: View {
#State private var presentNavView2 = false
#Binding var presentNavView1: Bool
var body: some View {
List {
NavigationLink(destination: NavView2(presentNavView2: self.$presentNavView2), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
}) // Button
}) // NavigationLink
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("Back")
})
} // List
.navigationTitle("NavView1")
} // View
}
NavView2
struct NavView2: View {
#Binding var presentNavView2: Bool
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("Back")
}) // Button
} // VStack
.navigationTitle("NavView2")
}
}
You can use DismissAction, because PresentationMode will be deprecated. I tried the code and it works perfectly! Here you go!
import SwiftUI
struct MContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
})
})
}
.navigationTitle("Home")
}
}
}
struct NavView1: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
#State private var presentNavView2 = false
var body: some View {
List {
NavigationLink(destination: NavView2(), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
})
})
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView1")
}
}
struct NavView2: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView2")
}
}
struct MContentView_Previews: PreviewProvider {
static var previews: some View {
MContentView()
}
}

How to pop to specific view in the TabView Application in swiftui. I used StackNavigation also but not working in swiftui

I am facing an issue while popping to a specific view. Let me explain the hierarchy.
ContentView -> 2 tabs, TabAView and TabBView
Inside TabBView. There is 1 view used ConnectView: Where is a Button to connect. After tapping on the button of Connect, the user move to another View which is called as UserAppView. From Here User can check his profile and update also. After the Update API call, need to pop to UserAppView from UserFirstFormView.
Here is the code to understand better my problem.
ContentView.swift
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
#ObservedObject var userViewModel: UserViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(userViewModel: userViewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
TabBView(userViewModel: userViewModel)
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var userViewModel: UserViewModel
#EnvironmentObject var settings: Settings
init(userViewModel: UserViewModel) {
self.userViewModel = userViewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is another TabBView:
struct TabBView: View {
#ObservedObject var userViewModel: UserViewModel
init(userViewModel: UserViewModel) {
self.userViewModel = userViewModel
}
var body: some View {
VStack (spacing: 10) {
NavigationLink(destination: ConnectView(viewModel: ConnectViewModel(id: id!), userViewModel: userViewModel)) {
UserCardWidget()
}
}
}
}
There is 1 connectView used on the TabBView through which the user will connect. ConnectViewModel is used here to call connect API.
class ConnectViewModel: ObservableObject {
var id: String?
init(id: String) {
self.id = id
}
func connect(completion: #escaping () -> Void) {
APIService.shared.connectApp(id: self.id!) { connected in
DispatchQueue.main.async {
self.isConnected = connected ?? false
completion()
}
}
}
}
This is connect view
struct ConnectView: View {
#ObservedObject var connectViewModel: ConnectViewModel
#ObservedObject var userViewModel: UserViewModel
#State var buttonTitle = "CONNECT WITH THIS"
#State var isShowingDetailView = false
var body: some View {
VStack {
Spacer()
if let id = connectViewModel.id {
NavigationLink(destination: UserAppView(id: id, userViewModel: userViewModel), isActive: $isShowingDetailView) {
Button(buttonTitle, action: {
connectViewModel.connect {
buttonTitle = "CONNECTED"
isShowingDetailView = true
}
})
}
}
}
}
}
This is the UserAppViewModel where API is hit to fetch some user-related details:
class UserAppViewModel: ObservableObject {
var id = ""
func getdetails() {
APIService.shared.getDetails() { userDetails in
DispatchQueue.main.async {
/// code
}
}
}
}
This is UserAppView class
struct UserAppView: View {
#ObservedObject var userViewModel: UserViewModel
#State private var signUpInButtonClicked: Bool = false
#StateObject private var userAppViewModel = UserAppViewModel()
init(id: String, userViewModel: UserViewModel) {
self.id = id
self.userViewModel = userViewModel
}
var body: some View {
VStack {
Text(userAppViewModel.status)
VStack {
Spacer()
NavigationLink(
destination: ProfileView(userAppViewModel: userAppViewModel, isActive: $signUpInButtonClicked)) { EmptyView() }
if /// Condition {
Button(action: {
signUpInButtonClicked = true
}, label: {
ZStack {
/// code
}
.frame(maxWidth: 77, maxHeight: 25)
})
}
}.onAppear(perform: {
**userAppViewModel.getDetails**(id: id)
})
}
}
From Here, the User Can Navigate to ProfileView.
struct ProfileUpdateView: View {
#State private var navigationSelectionFirstFormView = false
#State private var navigationSelectionLastFormView = false
public var body: some View {
VStack {
NavigationLink(destination: UserFirstFormView(userAppViewModel: userAppViewModel), isActive: $navigationSelectionFirstFormView) {
EmptyView()
}
NavigationLink(destination: UserLastFormView(userAppViewModel: userAppViewModel), isActive: $navigationSelectionLastFormView) {
EmptyView()
}
}
.navigationBarItems(trailing: Button(action: {
if Condition {
navigationSelectionFirstFormView = true
} else {
navigationSelectionLastFormView = true
}
}, label: {
HStack {
Text("Action")
.foregroundColor(Color.blue)
}
})
)
}
}
Further, their user will move to the next screen to update the profile.
struct UserFirstFormView: View {
var body: some View {
VStack {
/// code
///
Button("buttonTitle", action: {
API Call completion: { status in
if status {
self.rootPresentationMode.wrappedValue.dismiss()
}
})
})
.frame(maxHeight: 45)
}
}
}
I am trying to pop from this view once the API response is received but nothing is working.
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.
You could use the navigation link with, tag: and selection: overload and let the viewmodel control what link is open, here a example
enum profileViews {
case view1
case view2}
in your viewModel add an published var that will hold the active view
#Published var activeView: profileViews?
then in your navigation link you can do it like this
NavigationLink(
destination: secondView(profileViewModel: ProfileViewModel ),
tag: profileViews.view1,
selection: self.$profileViewModel.activeView
){}
Then you could pop any view just updating the variable inside the view model
self.profileViewModel.activeView = nil

Dismiss sheet SwiftUI

I'm trying to implement a dismiss button for my modal sheet as follows:
struct TestView: View {
#Environment(\.isPresented) var present
var body: some View {
Button("return") {
self.present?.value = false
}
}
}
struct DataTest : View {
#State var showModal: Bool = false
var modal: some View {
TestView()
}
var body: some View {
Button("Present") {
self.showModal = true
}.sheet(isPresented: $showModal) {
self.modal
}
}
}
But the return button when tapped does nothing. When the modal is displayed the following appears in the console:
[WindowServer] display_timer_callback: unexpected state (now:5fbd2efe5da4 < expected:5fbd2ff58e89)
If you force unwrap present you find that it is nil
How can I dismiss .sheet programmatically?
iOS 15+
Starting from iOS 15 we can use DismissAction that can be accessed as #Environment(\.dismiss).
There's no more need to use presentationMode.wrappedValue.dismiss().
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
Use presentationMode from the #Environment.
Beta 6
struct SomeView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Ohay!")
Button("Close") {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
For me, beta 4 broke this method - using the Environment variable isPresented - of using a dismiss button. Here's what I do nowadays:
struct ContentView: View {
#State var showingModal = false
var body: some View {
Button(action: {
self.showingModal.toggle()
}) {
Text("Show Modal")
}
.sheet(
isPresented: $showingModal,
content: { ModalPopup(showingModal: self.$showingModal) }
)
}
}
And in your modal view:
struct ModalPopup : View {
#Binding var showingModal:Bool
var body: some View {
Button(action: {
self.showingModal = false
}) {
Text("Dismiss").frame(height: 60)
}
}
}
Apple recommend (in WWDC 2020 Data Essentials in SwiftUI) using #State and #Binding for this. They also place the isEditorPresented boolean and the sheet's data in the same EditorConfig struct that is declared using #State so it can be mutated, as follows:
import SwiftUI
struct Item: Identifiable {
let id = UUID()
let title: String
}
struct EditorConfig {
var isEditorPresented = false
var title = ""
var needsSave = false
mutating func present() {
isEditorPresented = true
title = ""
needsSave = false
}
mutating func dismiss(save: Bool = false) {
isEditorPresented = false
needsSave = save
}
}
struct ContentView: View {
#State var items = [Item]()
#State private var editorConfig = EditorConfig()
var body: some View {
NavigationView {
Form {
ForEach(items) { item in
Text(item.title)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: presentEditor) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $editorConfig.isEditorPresented, onDismiss: {
if(editorConfig.needsSave) {
items.append(Item(title: editorConfig.title))
}
}) {
EditorView(editorConfig: $editorConfig)
}
}
}
func presentEditor() {
editorConfig.present()
}
}
struct EditorView: View {
#Binding var editorConfig: EditorConfig
var body: some View {
NavigationView {
Form {
TextField("Title", text:$editorConfig.title)
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(action: save) {
Text("Save")
}
.disabled(editorConfig.title.count == 0)
}
ToolbarItem(placement: .cancellationAction) {
Button(action: dismiss) {
Text("Dismiss")
}
}
}
}
}
func save() {
editorConfig.dismiss(save: true)
}
func dismiss() {
editorConfig.dismiss()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(items: [Item(title: "Banana"), Item(title: "Orange")])
}
}

.sheet: Shows only once and then never again

Working with Beta4, it seems that the bug is still existing. The following sequence of views (a list, where a tap on a list entry opens another list) allows to present the ListView exactly once; the onDisappear is never called, so the showModal flag changes, but does not triggers the redisplay of ListView when tapped again. So, for each GridCellBodyEntry, the .sheet presentation works exactly once, and then never again.
I tried around with several suggestions and workarounds, but none worked (e.g., encapsulating with a NavigationViewModel). I even tried to remove the List, because there was an assumption that the List causes that behaviour, but even this did not change anything.
Are there any ideas around?
The setup:
A GridCellBody with this view:
var body: some View {
GeometryReader { geometry in
VStack {
List {
Section(footer: self.footerView) {
ForEach(self.rawEntries) { rawEntry in
GridCellBodyEntry(entityType: rawEntry)
}
}
}
.background(Color.white)
}
}
}
A GridCellBodyEntry with this definition:
struct GridCellBodyEntry: View {
let entityType: EntityType
let viewModel: BaseViewModel
init(entityType: EntityType) {
self.entityType = entityType
self.viewModel = BaseViewModel(entityType: self.entityType)
}
#State var showModal = false {
didSet {
print("showModal: \(showModal)")
}
}
var body: some View {
Group {
Button(action: {
self.showModal.toggle()
},
label: {
Text(entityType.localizedPlural ?? "")
.foregroundColor(Color.black)
})
.sheet(isPresented: $showModal, content: {
ListView(showModal: self.$showModal,
viewModel: self.viewModel)
})
}.onAppear{
print("Profile appeared")
}.onDisappear{
print("Profile disappeared")
}
}
}
A ListView with this definition:
struct ListView: View {
// MARK: - Private properties
// MARK: - Public interface
#Binding var showModal: Bool
#ObjectBinding var viewModel: BaseViewModel
// MARK: - Main view
var body: some View {
NavigationView {
VStack {
List {
Section(footer: Text("\(viewModel.list.count) entries")) {
ForEach(viewModel.list, id: \.objectID) { item in
NavigationLink(destination: ItemView(),
label: {
Text("\(item.objectID)")
})
}
}
}
}
.navigationBarItems(leading:
Button(action: {
self.showModal = false
}, label: {
Text("Close")
}))
.navigationBarTitle(Text(viewModel.entityType.localizedPlural ?? ""))
}
}
}
The BaseViewModel (excerpt):
class BaseViewModel: BindableObject {
/// The binding support.
var willChange = PassthroughSubject<Void, Never>()
/// The context.
var context: NSManagedObjectContext
/// The current list of typed items.
var list: [NSManagedObject] = []
// ... other stuff ...
}
where willChange.send() is called whenever something changes (create, modify, delete operations).
This is a variant of swiftUI PresentaionLink does not work second time
The following simplified code exhibits the behavior you're experiencing (the sheet only displays once):
import SwiftUI
struct ContentView: View {
#State var isPresented = false
#State var whichPresented = -1
var body: some View {
NavigationView {
List {
ForEach(0 ..< 10) { i in
Button(action: {
self.whichPresented = i
self.isPresented.toggle()
})
{ Text("Button \(i)") }
}.sheet(isPresented: $isPresented, content: {
Text("Destination View \(self.whichPresented)") })
}
}
}
}
There appears to be a bug in SwiftUI when you put the .sheet inside a List or a ForEach. If you move the .sheet outside of the List, you should be able to get the correct behavior.
import SwiftUI
struct ContentView: View {
#State var isPresented = false
#State var whichPresented = -1
var body: some View {
NavigationView {
List {
ForEach(0 ..< 10) { i in
Button(action: {
self.whichPresented = i
self.isPresented.toggle()
})
{ Text("Button \(i)") }
}
}
}.sheet(isPresented: $isPresented, content: { Text("Destination View \(self.whichPresented)") })
}
}