SwiftUI navigation works on iOS but fails on macOS - swift

I have an iOS SwiftUI app that works fine on iPhone and iPad, but when compiled for native macOS the navigation links are disabled. Code is below. Briefly, the FirstView is a NavigationView containing a List of NavigationLinks. These work, and link to a DetailView that contains a next level of NavigationLinks presented as Images.
On iOS, these images are active blue that when clicked, go to a FinalView.
On macOS, these images are inactive: Presented as gray, and don't have any click-ability.
Why do these appear differently on the two OS's? How to fix the macOS version so that it navigates? Thanks in advance
import SwiftUI
struct TNode { // simple data struct that just carries name and ID
public let id = UUID()
public let name : String
init( _ name:String) {
self.name = name
}
}
// starting view
struct FirstView: View {
init () {
// provide some sample data
nodes = [TNode("node 1"), TNode("node 2")]
}
#State private var nodes : [TNode]
var body: some View {
NavigationView {
List {
ForEach(nodes, id:\.id) { (node) in
// where to go when selected
NavigationLink(destination:
DetailView(displayNode: node)
){ // display names for selection in the sidebar
VStack {
Text(node.name)
}
}
}
}
}
}
}
struct DetailView: View {
let displayNode : TNode
var body: some View {
VStack(alignment: .leading) {
Text(displayNode.name)
HStack{
// this is what doesn't work on macOS
NavigationLink(destination: FinalView()) {
Image(systemName:"figure.stand.line.dotted.figure.stand")
}
NavigationLink(destination: FinalView()) {
Image(systemName:"square.and.pencil")
}
}
}
}
}
struct FinalView : View {
var body : some View {
Text("Got to final view OK")
}
}
Behavior on macOS:
Xcode 13.4.1, macOS Monterey 12.4

Embed the VStack of the DetailView in a NavigationView. Remember that iOS is different than macOS.
In iOS you didn't need to wrap the child View of in a NavigationView because it's a child. With macOS this a standalone View.

Remove Stack in Navigation link :
){ // display names for selection in the sidebar
// Remove VStack VStack {
Text(node.name)
// }
}

Related

SwiftUI App works fine in simulator, but not in preview

I'm trying to get preview working in Xcode. Just a simple test app written in Swift. Grabs rows from a Realm database and lists them.
It works fine when I build/run in the simulator but none of the data shows in the ContentView_Preview.
App in Simulator
App in Preview
Here's the code:
import SwiftUI
import RealmSwift
struct HeaderView: View {
var body: some View {
HStack(alignment: .firstTextBaseline) {
Text("Time Spent").font(.largeTitle)
Text("or waisted...").font(.headline)
}
}
}
struct GroupListView: View {
#ObservedResults(TaskGroup.self) var taskGroups
var body: some View {
NavigationView {
// Nothing from here shows in the preview.. but shows fine in the simulator
List(taskGroups) {
Text("Group: " + $0.name)
}
.navigationTitle("Task Groups (\(taskGroups.count))")
// Grrr
}
}
}
struct FooterView: View {
var body: some View {
HStack {
Text("🤬 Grr.. Preview isn't working.")
Spacer()
Text("Simulator works fine though.")
}
}
}
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
HeaderView()
GroupListView()
FooterView()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I've searched for hours and tried various different suggestions. No luck. Any help would be wonderful.
In your preview it's likely that taskGroups is empty. You could verify this by sticking something like
if taskGroups.isEmpty {
Text("test")
}
For your preview you should inject some placeholder content. Depending on how Realm works there may be a mechanism to do this already, or you may need to create an abstraction that allows you to do this.

SwiftUI sheet gets dismissed the first time it is presented

This bug is driving me insane. Sometimes (well most of the time) presented sheet gets dismissed first time it is opened. This is happening only on a device and only the first time the app is launched. Here is how it looks on iPhone 11 running iOS 14.1 built using Xcode 12.1 (can be reproduced on iPhone 7 Plus running iOS 14.0.1 as well):
Steps in the video:
I open app
Swiftly navigate to Details view
Open Sheet
Red Sheed gets dismissed by the system???
I open sheet again and it remains on the screen as expected.
This is SwitUI App project (using UIKIt App Delegate) and deployment iOS 14. Code:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailsView()) {
Text("Open Details View")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailsView: View {
#State var sheetIsPresented: Bool = false
var body: some View {
VStack {
Button("Open") {
sheetIsPresented.toggle()
}
}.sheet(isPresented: $sheetIsPresented, content: {
SheetView()
})
}
}
struct SheetView: View {
var body: some View {
Color.red
}
}
I was able to fix this problem by removing line .navigationViewStyle(StackNavigationViewStyle()), but I need StackNavigationViewStyle in my project. Any help will be appreciated.
Updated: There is a similar post on Apple forum with sheet view acting randomly weird.
One solution that I found is to move sheet to the root view outside NavigationLink (that would be ContentView in my example), but that is not ideal solution.
I had the same problem in an app. After a great deal of research, I found that making the variable an observed object fixed the problem in SwiftUI 1, and it seems to be in SwiftUI 2. I do remember that it was an intermittent problem on an actual device, but it always happened in the simulator. I wish I could remember why, maybe when the sheet appears it resets the bound variable?, but this code fixes the problem:
import SwiftUI
import Combine
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailsView()) {
Text("Open Details View")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailsView: View {
#ObservedObject var sheetIsPresented = SheetIsPresented.shared
var body: some View {
VStack {
Button("Open") {
sheetIsPresented.value.toggle()
}
}.sheet(isPresented: $sheetIsPresented.value, content: {
SheetView()
})
}
}
struct SheetView: View {
var body: some View {
Color.red
}
}
final class SheetIsPresented: NSObject, ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
static let shared = SheetIsPresented()
#Published var value: Bool = false {
willSet {
objectWillChange.send()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Tested on Xcode 12.1, iOS 14.1 in simulator.
Just add your NavigationView view to bellow code line
.navigationViewStyle(StackNavigationViewStyle())
Example :
NavigationView { }.navigationViewStyle(StackNavigationViewStyle())
Problem will be solved (same issues I was faced)

SwiftUI - Conditional based on ObservedObject doesn't work in subsequent Views

I have a very simple app which contains two views that are tied together with a NavigationLink. In the first view, ContentView, I can see updates to my ObservedObject as expected. However, when I go to the next View, it seems that the code based on the ObservedObject does not recognize changes.
ContentView.swift (The working view):
import SwiftUI
struct ContentView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
NavigationView {
VStack(spacing: 15) {
Toggle(isOn: self.$toggleObject.isToggled) {
Text("Toggle:")
}
if self.toggleObject.isToggled == true {
Text("ON")
}
else {
Text("OFF")
}
NavigationLink(destination: ShowToggleView()) {
Text("Show Toggle Status")
}
}
}
}
}
ShowToggleView.swift (The view that does not behave as I expect it to):
import SwiftUI
struct ShowToggleView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
Form {
Section {
if self.toggleObject.isToggled {
Text("Toggled on")
}
else {
Text("Toggled off")
}
}
}
}
}
All of this data is stored in a simple file, ToggleObject.swift:
import SwiftUI
class ToggleObject: ObservableObject {
#Published var isToggled = false
}
When I toggle it on in the first View I see the text "ON" which is expected, but when I go into the next view it shows "Toggled off" no matter what I do in the first View... Why is that?
Using Xcode 11.5 and Swift 5
You are almost doing everything correct. However, you are creating another instance of ToggleObject() in your second view, which overrides the data. You basically only create one ObservedObject and then pass it to your subview, so they both access the same data.
Change it to this:
struct ShowToggleView: View {
#ObservedObject var toggleObject : ToggleObject
And then pass the object to that view in your navigation link...
NavigationLink(destination: ShowToggleView(toggleObject: self.toggleObject)) {
Text("Show Toggle Status")
}

Why does modifying the label of a NavigationLink change which View is displayed in SwiftUI?

I have an #EnvironmentObject called word (of type Word) whose identifier property I'm using for the label of a NavigationLink in SwiftUI. For the DetailView that is linked to the NavigationLink, all I have put is this:
struct DetailView: View {
#EnvironmentObject var word: Word
var body: some View {
VStack {
Text(word.identifier)
Button(action: {
self.word.identifier += "a"
}) {
Text("Click to add an 'a' to Word's identifier")
}
}
}
}
The ContentView that leads to this DetailView looks like this (I've simplified my actual code to isolate the problem).
struct ContentView: View {
#EnvironmentObject var word: Word
var body: some View {
NavigationView {
NavigationLink(destination: DetailView()) {
Text(word.identifier)
}
}
}
}
When I tap the button on the DetailView, I'd expect it to update the DetailView with a new word.identifier that has an extra "a" appended onto it. When I tap it, however, it takes me back to the ContentView, albeit with an updated word.identifier. I can't seem to find a way to stay on my DetailView when the word.identifier being used by the ContentView's NavigationLink is modified. Also, I am running Xcode 11.3.1 and am currently unable to update, so if this is has been patched, please let me know.
Here is workaround solution
struct DetailView: View {
#EnvironmentObject var word: Word
#State private var identifier: String = ""
var body: some View {
VStack {
Text(self.identifier)
Button(action: {
self.identifier += "a"
}) {
Text("Click to add an 'a' to Word's identifier")
}
}
.onAppear {
self.identifier = self.word.identifier
}
.onDisappear {
self.word.identifier = self.identifier
}
}
}
This works as expected on iOS 13.4, assuming Word is something like:
class Word : ObservableObject {
#Published var identifier = "foo"
}

SwiftUI: Dismiss View Within macOS NavigationView

As detailed here (on an iOS topic), the following code can be used to make a SwiftUI View dismiss itself:
#Environment(\.presentationMode) var presentationMode
// ...
presentationMode.wrappedValue.dismiss()
However, this approach doesn't work for a native (not Catalyst) macOS NavigationView setup (such as the below), where the selected view is displayed alongside the List.
Ideally, when any of these sub-views use the above, the list would go back to having nothing selected (like when it first launched); however, the dismiss function appears to do nothing: the view remains exactly the same.
Is this a bug, or expected macOS behaviour?
Is there another approach that can be used instead?
struct HelpView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination:
AboutAppView()
) {
Text("About this App")
}
NavigationLink(destination:
Text("Here’s a User Guide")
) {
Text("User Guide")
}
}
}
}
}
struct AboutAppView: View {
#Environment(\.presentationMode) var presentationMode
public var body: some View {
Button(action: {
self.dismissSelf()
}) {
Text("Dismiss Me!")
}
}
private func dismissSelf() {
presentationMode.wrappedValue.dismiss()
}
}
FYI: The real intent is for less direct scenarios (such as triggering from an Alert upon completion of a task); the button setup here is just for simplicity.
The solution here is simple. Do not use Navigation View where you need to dismiss the view.
Check the example given by Apple https://developer.apple.com/tutorials/swiftui/creating-a-macos-app
If you need dismissable view, there is 2 way.
Create a new modal window (This is more complicated)
Use sheet.
Following is implimenation fo sheet in macOS with SwiftUI
struct HelpView: View {
#State private var showModal = false
var body: some View {
NavigationView {
List {
NavigationLink(destination:
VStack {
Button("About"){ self.showModal.toggle() }
Text("Here’s a User Guide")
}
) {
Text("User Guide")
}
}
}
.sheet(isPresented: $showModal) {
AboutAppView(showModal: self.$showModal)
}
}
}
struct AboutAppView: View {
#Binding var showModal: Bool
public var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Dismiss Me!")
}
}
}
There is also a 3rd option to use ZStack to create a Modal Card in RootView and change opacity to show and hide with dynamic data.