Removing SwiftUI NavigationView from view hierarchy result in EXC_BAD_ACCESS - swiftui

I am struggling with a bug and I just can't seem to solve it, or where to look further.
The problem occurs when I try to remove a view (which holds a NavigationView) from the view hierarchy. It crashes with: Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
After experimenting with the sanitizer I got this output in the debugger: *** -[_TtGC7SwiftUI41StyleContextSplitViewNavigationControllerVS_19SidebarStyleContext_ removeChildViewController:]: message sent to deallocated instance 0x10904c880
Which pointed me to figure out that it was the NavigationView that cause it somehow. But I still can't figure out how to get from here.
This problem ONLY occurs on a real device, it works just fine in a simulator and you may have to hit the login, and then log out and log back in a few times before the crash happens.
I made a sample app with the example: https://github.com/Surferdude667/NavigationRemoveTest
The code is as follows:
NavigationRemoveTestApp
#main
struct NavigationRemoveTestApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
RootView
struct RootView: View {
#StateObject private var viewModel = RootViewModel()
var body: some View {
if !viewModel.loggedIn {
WelcomeView()
} else {
ContentView()
}
}
}
RootViewModel
class RootViewModel: ObservableObject {
#Published var loggedIn = false
init() {
LogInController.shared.loggedIn
.receive(on: DispatchQueue.main)
.assign(to: &$loggedIn)
}
}
WelcomeView
struct WelcomeView: View {
var body: some View {
NavigationView {
VStack {
Text("Welcome")
NavigationLink("Go to login") {
LogInView()
}
}
}
}
}
LogInView
struct LogInView: View {
var body: some View {
VStack {
Text("Log in view")
Button("Log in") {
LogInController.shared.logIn()
}
}
}
}
ContentView
struct ContentView: View {
var body: some View {
VStack {
Text("Content view")
Button("Log out") {
LogInController.shared.logOut()
}
}
}
}
LogInController
import Combine
class LogInController {
static let shared = LogInController()
var loggedIn: CurrentValueSubject<Bool, Never>
private init() {
self.loggedIn = CurrentValueSubject<Bool, Never>(false)
}
func logIn() {
self.loggedIn.send(true)
}
func logOut() {
self.loggedIn.send(false)
}
}

I found a few solutions.
Either you wrap the if statement in the RootView with a NavigationView instead of having the NavigationView inside the actual views, it works. This is however not very convenient since everything is now wrapped in a NavigationView.
Replacing NavigationView with the new iOS 16 NavigationStack also solves it.

Omg mate, I had the same problem, because of this post it's solved. But don't you think it is a really weird bug? Did you find out more about the root cause? Nevertheless you made my day.

Related

NavigationStack not affected by EnvironmentObject changes

I'm attempting to use #EnvironmentObject to pass an #Published navigation path into a SwiftUI NavigationStack using a simple wrapper ObservableObject, and the code builds without issue, but working with the #EnvironmentObject has no effect. Here's a simplified example that still exhibits the issue:
import SwiftUI
class NavigationCoordinator: ObservableObject {
#Published var path = NavigationPath()
func popToRoot() {
path.removeLast(path.count)
}
}
struct ContentView: View {
#StateObject var navigationCoordinator = NavigationCoordinator()
var body: some View {
NavigationStack(path: $navigationCoordinator.path, root: {
FirstView()
})
.environmentObject(navigationCoordinator)
}
}
struct FirstView: View {
var body: some View {
VStack {
NavigationLink(destination: SecondView()) {
Text("Go To SecondView")
}
}
.navigationTitle(Text("FirstView"))
}
}
struct SecondView: View {
var body: some View {
VStack {
NavigationLink(destination: ThirdView()) {
Text("Go To ThirdView")
}
}
.navigationTitle(Text("SecondView"))
}
}
struct ThirdView: View {
#EnvironmentObject var navigationCoordinator: NavigationCoordinator
var body: some View {
VStack {
Button("Pop to FirstView") {
navigationCoordinator.popToRoot()
}
}
.navigationTitle(Text("ThirdView"))
}
}
I am:
Passing the path into the NavigationStack path parameter
Sending the simple ObservableObject instance into the NavigationStack via the .environmentObject() modifier
Pushing a few simple child views onto the stack
Attempting to use the environment object in ThirdView
NOT crashing when attempting to use the environment object (e.g. "No ObservableObject of type NavigationCoordinator found")
Am I missing anything else that would prevent the deeply stacked view from using the EnvironmentObject to affect the NavigationStack's path? It seems like the NavigationStack just isn't respecting the bound path.
(iOS 16.0, Xcode 14.0)
The reason your code is not working is that you haven't added anything to your path, so your path is empty. You can simply verify this by adding print(path.count) in your popToRoot method it will print 0 in the console.
To work with NavigationPath you need to use navigationDestination(for:destination:) ViewModifier, So for your example, you can try something like this.
ContentView:- Change NavigationStack like this.
NavigationStack(path: $navigationCoordinator.path) {
VStack {
NavigationLink(value: 1) {
Text("Go To SecondView")
}
}
.navigationDestination(for: Int.self) { i in
if i == 1 {
SecondView()
}
else {
ThirdView()
}
}
}
SecondView:- Change NavigationLink like this.
NavigationLink(value: 2) {
Text("Go To ThirdView")
}
This workaround works with Int but is not a better approach, so my suggestion is to use a custom Array as a path. Like this.
enum AppView {
case second, third
}
class NavigationCoordinator: ObservableObject {
#Published var path = [AppView]()
}
NavigationStack(path: $navigationCoordinator.path) {
FirstView()
.navigationDestination(for: AppView.self) { path in
switch path {
case .second: SecondView()
case .third: ThirdView()
}
}
}
Now change NavigationLink in FirstView and SecondView like this.
NavigationLink(value: AppView.second) {
Text("Go To SecondView")
}
NavigationLink(value: AppView.third) {
Text("Go To ThirdView")
}
The benefit of the above is now you can use the button as well to push a new screen and just need to append in your path.
path.append(.second)
//OR
path.append(.third)
This will push a respected view.
For more details, you can read the Apple document of NavigationLink and NavigationPath.

SwiftUI: Updating ui when view is not present causes "Unable to present view. Please file a bug."

I get the following error: Unable to present view. Please file a bug whenever I make an asynchronous call on a view and leave the view (e.g. navigate to another view in the navigation stack) before it can make changes to the ui. Consequently, the next view in the navigation stack is unable to update its view. How can I fix this problem?
An example of the problem occurring is when I switch from view1 to view2 before my GetIoTThingIndex() call finishes and makes an update to the ui.
GetIoTThingIndex.query(device) { error in
DispatchQueue.main.async { [self] in
...
}
}
EDIT:
After doing more investigating, I found that this problem is due to the fact that I am implementing my logic in an MVVM pattern. When I moved my logic directly into the the view and called the functions and state variables inside the view, everything worked fine. It's interesting because when I started building my app with just a few pages with minimal logic and dependencies, this MVVM pattern worked fine without any bugs. However, when my project grew to 20+ pages with more logic and dependencies, the MVVM pattern causes this bug. Is this just a problem I see or has anyone seen anything like this before and have any recommendations for fixing it?
This is the way I had things with MVVM.
View
struct DeviceView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
Text(viewModel.name)
...
}
}
View Model
class DeviceViewModel: ObservableObject {
#Published var name = ""
public func updateUI() {
...
}
...
}
This is the way I have things now (which works without this bug).
View
struct DeviceView: View {
var body: some View {
Text(name)
...
}
#State var name = ""
public func updateUI() {
...
}
...
}
Are you sure this is what is happening?
I've tested the idea of navigating to another view
before the parent can make a change to its view. And all works well.
This is the code I used for the test, click on the button first, then within 3 sec click on the NavigationLink.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var thingToUpdate = ""
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("text \(thingToUpdate)")
Button("click me first") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
thingToUpdate = " is updated now"
}
}
NavigationLink(destination: Text("the detail view")) {
Text("then to DetailView")
}
}
}
}
}
Edit update using ObservableObject that works for me:
class DeviceViewModel: ObservableObject {
#Published var name = "no name"
public func updateUI() {
// simulated delay on the main thread
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.name = "success"
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("viewModel name is \(viewModel.name)")
Button("click me first") {
viewModel.updateUI()
}
NavigationLink(destination: Text("DetailView")) {
Text("then to DetailView")
}
}
}
}
}

PresentationMode.dismiss weird behaviour when using multiple NavigationLinks inside ForEach

My app has 4 views (let's call them View_A[root] -> View_B -> View_C -> View_D). The navigation between them was made using NavigationView/NavigationLink.
When I call self.presentationMode.wrappedValue.dismiss() from the last view(View_D) I expect it to dismiss the current view (D) only, but for some reason it dismissed ALL the views and stops at view A (root view).
That's weird.
I spent a couple of hours trying to figure out what's going on there and I found that
- if I remove "ForEach" from "View_A" it works correctly and only the last view is dismissed. Even though ForEach gets just 1 static object in this example.
The second weird thing is that
- if I don't change "self.thisSession.stats" to false it also works correctly dismissing only the last view.
This is super weird as View_A (as far as I understand) is not dependent on thisSession environment variable.
Any ideas on how to prevent View_C and View_B from being dismissed in this case? I wanna end up at View_C after clicking the link, not at View_A.
Any help is appreciated, it took me a while to find out where it comes from but I'm not smart enough to proceed any further ;)
import SwiftUI
struct A_View: View {
#EnvironmentObject var thisSession: CurrentSession
var body: some View {
NavigationView {
VStack {
Text("View A")
ForEach([TestObject()], id: \.id) { _ in
NavigationLink(destination: View_B() ) {
Text("Move to View B")
}
}
}
}
}
}
struct View_B: View {
var body: some View {
NavigationView {
NavigationLink(destination: View_C()
) {
Text("GO TO VIEW C")
}
}
}
}
struct View_C: View {
var body: some View {
ZStack {
NavigationView {
NavigationLink(destination: View_D()) {
Text("GO TO VIEW D")
}
}
}
}
}
struct View_D: View {
#EnvironmentObject var thisSession: CurrentSession
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
ZStack {
VStack {
Button(action: {
self.thisSession.stats = false
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Return!")
}
}
}
}
}
class CurrentSession: ObservableObject {
#Published var stats: Bool = false
#Published var user: String = "user"
}
struct TestObject: Identifiable, Codable {
let id = UUID()
}
Your issue is with:
NavigationView
There is only supposed to be one NavigationView in an entire view stack. Try removing the NavigationView from views B and C

How can I show a page depending of child button clicked with SwiftUI?

I am trying to rewrite my app using SwiftUI only and I am having difficulty with the EnvironmentObject, trying to understand how it works…
I want to redirect my app users to the appropriate page at launch, depending on:
if this is their first time
if they have a login,
if they want to start using without login
If it is the first time the app is launched, LocalStorage has no data so I present the app on a welcome page
I offer the choice of 2 buttons to click on:
“New User” which redirect to the main page of the app and create a new user
“Login” which present the login page to retrieve the last backup
If the app has previously been launched, I present the main page straight away.
Now said, if I initiate my “currentPage” as “MainView” or “LoginView”, it works - but NOT if it is set as “WelcomeView”.
I presume the problem comes when the variable gets changed from a subview? I thought the use of #EnvironmentObject was the way to get around this…
Can someone explain to me how it works?
My various files are:
import SwiftUI
import Combine
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "WelcomeView" {
didSet {
objectWillChange.send(self)
}
}
}
import SwiftUI
struct ParentView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "WelcomeView" {
WelcomeView()
}
else if viewRouter.currentPage == "MainView" {
MainView()
}
else if viewRouter.currentPage == "LoginView" {
LoginView()
}
}
}
}
import SwiftUI
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons().environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}
import SwiftUI
struct LoginButtons: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {
self.viewRouter.currentPage = "MainView"
}) {
Text("NEW USER")
}
Button(action: {
self.viewRouter.currentPage = "LoginView"
}) {
Text("I ALREADY HAVE AN ACCOUNT")
}
}
}
}
import SwiftUI
struct MainView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
import SwiftUI
struct LoginView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
Many Thanks in advance! :wink:
Ok so in your main view, the one that you are going to decide where to send your user, you could check for the app if it was lunched before or not, depending on that do whatever you want. Once you know how to do this, you can adapt to the other things. This is how you can check for it, again, in your main view router:
init() {
// Create initial Data if not data has been setup
if (InitialAppSetup().initialDataLoaded == false) {
InitialAppSetup().createInitialData()
}
// Onboarding screen
if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
UserDefaults.standard.set(true, forKey: "didLaunchBefore")
currentPage = "onboardingView"
} else {
currentPage = "homeView"
}
}
The InitialAppSetup() class has a UserDefault which goes like this:
#Published var initialDataLoaded: Bool = UserDefaults.standard.bool(forKey: "InitialData") {
didSet {
UserDefaults.standard.set(self.initialDataLoaded, forKey: "InitialData")
}
}
Ok... My 'mistake' was to add an extra ".environmentObject(ViewRouter())" when calling my subview "LoginButtons".
If I remove it, it works!.. But why?!?
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons()
// --> .environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}

SwiftUI - View not refreshing when a property has changed

I'd appreciate any help with this, I'm just a beginner so I'm not sure if I'm misunderstanding, or have the implementation wrong or if this is a bug.
I'm trying to have my MacOS app segue to the main screen after a successful login. I have an appState to share state with the rest of the app. The AppState is a class conforming to Observable object and I added an observer to print whenever the isLoggedIn property changes:
class AppState : ObservableObject {
#Published var isLoggedIn = false {
didSet {
print("AppState isLoggedin: \(isLoggedIn)")
}
}
}
I also have a MasterView struct to deal with changing the main view.
struct MasterView: View {
#ObservedObject var appState: AppState = AppState()
var body: some View {
return Group {
if appState.isLoggedIn {
NavView()
} else {
LoginView()
}
}.frame(width: 1200, height: 800)
}
}
I have a bunch of code to handle doing the login which I won't post for the sake of brevity, suffice to say that it works, isLoggedIn is set to true and prints to the console after a successful login. The issue is that the view never updates to reflect this so I'm still stuck on the login screen.
Any help is greatly appreciated, I've spent more time on this than I care to admit. Thanks!
Update: I remember having trouble with #EnvironmentObject and so I switched to #ObservableObject and #Published. After re-implementing #EnvironmentObject I now remember why: I have a networking class which causes a crash as it is not an ancestor view. Per Paul Hudson's comment, "Note: Environment objects must be supplied by an ancestor view – if SwiftUI can’t find an environment object of the correct type you’ll get a crash. This applies for previews too, so be careful."
For More Information.
I figured it out, working code below.
AppState:
final class AppState : ObservableObject {
#Published var isLoggedIn = false {
didSet {
print("AppState isLoggedIn: \(isLoggedIn)")
}
}
}
Content View :
struct ContentView: View {
#ObservedObject var appState: AppState
var body: some View {
return Group {
if appState.isLoggedIn {
MainView(appState: appState)
} else {
LoginView(appState: appState)
}
}.frame(maxWidth: 1200, maxHeight: 800)
}
}
Login View:
struct LoginView: View {
#ObservedObject var appState: AppState
var body: some View {
Button(action: {
withAnimation {
self.appState.isLoggedIn.toggle()
}
}) {
Text("Go to Main View")
}.padding()
}
}
Finally, Main View:
struct MainView: View {
#ObservedObject var appState: AppState
var body: some View {
Button(action: {
withAnimation {
self.appState.isLoggedIn.toggle()
}
}) {
Text("Back To Login View")
}.padding()
}
}