Trigger Navigation from Context Menu in SwiftUI - swiftui

I have a List that contains NavigationLink inside a NavigationView.
I know want to extend the view with a ContextMenu that contains an element that shows another view inside my navigation stack.
struct MainView: View {
#State var elements = ["Hello", "World"]
var body: some View {
NavigationView {
List(elements, id: \.self, rowContent: { element in
NavigationLink(destination: PresentView(element: element)) {
Text(element)
.contextMenu {
NavigationLink(
"Edit",
destination: EditView(element: element)
)
}
}
})
}
}
}
The navigation for a normal tap on my item works fine. The context menu however stopped working in Xcode 11 Beta 5. I get the following error: `[WindowServer] display_timer_callback: unexpected state.
How would I push a new view on my navigation stack from a context menu?

One approach is to use NavigationLink(destination: Destination, isActive: Binding<Bool>, #ViewBuilder label: () -> Label), the label as an EmptyView hidden inside a ZStack. You would then select the element to navigate to and toggling the NavigationLink inside the contextMenu. Here is a full example:
struct PresentView: View {
let element: String
var body: some View {
Text(element)
}
}
struct EditView: View {
let element: String
var body: some View {
Text("EditView for \(element)")
}
}
struct MainView: View {
#State var elements = ["Hello", "World"]
#State var elementToEdit: String?
#State var isPresentedEditView = false
var body: some View {
NavigationView {
ZStack {
NavigationLink(
destination: elementToEdit == nil ? AnyView(EmptyView()) : AnyView(EditView(element: elementToEdit!)),
isActive: $isPresentedEditView) {
EmptyView()
}
List(elements, id: \.self) { element in
NavigationLink(destination: PresentView(element: element)) {
Text(element)
.contextMenu {
Button("Edit") {
elementToEdit = element
isPresentedEditView.toggle()
}
}
}
}
}
}
}
}
struct ContentView: View {
var body: some View {
MainView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

SwiftUI - Navigation title background becomes transparent when VStack is visible

I'm running into an issue with the navigation title header in SwiftUI. It's a combination of a couple of things, as far as I can tell...
The main problem is that I'm trying to change the default background color of a view that contains a list. But when I use the tag .background(), the navigation title background becomes transparent. This only happens when there is a VStack on the view.
I have a simplify example code that shows the problem I'm facing:
ContentView:
import SwiftUI
struct ContentView: View {
#State var showButton: Bool
var body: some View {
VStack {
NavigationStack {
NavigationLink(
destination: SecondView(showButton: showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView:
import SwiftUI
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
if showButton {
button
}
}
.navigationTitle("This is a title")
.background(Color(.systemCyan))
}
var button: some View {
Text("Something")
}
}
Please see below the resulting problem:
Issues / Suggestions:
ContentView
Have the NavigationStack outside the VStack
SecondView
Don't embed List inside a VStack
List is special and has special characteristics
Don't initialise #State property from outside, pass a binding instead
Code:
ContentView:
struct ContentView: View {
#State var showButton = true
var body: some View {
NavigationStack {
VStack {
NavigationLink(
destination: SecondView(showButton: $showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#Binding var showButton: Bool
var body: some View {
List {
ForEach(0..<100) { _ in
Text("Hello World")
}
}
.safeAreaInset(edge: .bottom) {
if showButton {
HStack {
Spacer()
button
Spacer()
}
//I have added transparency, you can make it opaque if you want
.background(.cyan.opacity(0.8))
}
}
}
var button: some View {
Text("Something")
}
}
Try this if you don't want your list go under nav bar.
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
.padding(.top, 1)
if showButton {
button
}
}
.background(Color(.systemCyan))
.navigationTitle("This is a title")
}
var button: some View {
Text("Something")
}
}

NavigationView usage in swiftUI

Coming from Android and working on a very complex application , i would like to use NavigationView as much as possible. Having one view and make all elements appear and disappear on this view seems impossible to handle for me .
I was using navigationView to navigate bewteen views with navigationBar hidden .
This way navigating or making view appear is transparent for the user
After some tests , i encounter limitations : at the 13th or 14 th level of navigation everything disappear and app basically crashes .
Once more , this is a direct navigation between 2 content views , no HOMESCREEN
import SwiftUI
struct test4: View {
#State private var intent3: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination : test3() , isActive : $intent3) { }
Text("ver 4")
.onTapGesture {
intent3 = true }
Spacer()
}
}
.navigationBarHidden(true)
}
}
import SwiftUI
struct test3: View {
#State private var intent4: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination : test4() , isActive : $intent4) { }
Text("ver 3")
.onTapGesture {
intent4 = true }
Spacer()
}
}.navigationBarHidden(true) }
}
Here a basic example of navigation directly between 2 contents views . Crashes after 14/15 clicks. I encounter the same issue with about any navigation link.
Update:
With your added code, I can see the initial crash was a result of adding a new NavigationView each time. This solves it:
struct ContentView: View {
var body: some View {
NavigationView {
Test3()
}
}
}
struct Test4: View {
#State private var intent3: Bool = false
var body: some View {
VStack{
NavigationLink(destination : Test3() , isActive : $intent3) { }
Text("ver 4")
.onTapGesture {
intent3 = true
}
Spacer()
}
.navigationBarHidden(true)
}
}
struct Test3: View {
#State private var intent4: Bool = false
var body: some View {
VStack{
NavigationLink(destination : Test4() , isActive : $intent4) { }
Text("ver 3")
.onTapGesture {
intent4 = true }
Spacer()
}
.navigationBarHidden(true)
}
}
Original answer:
However, there are solutions to pop to the top of a navigation hierarchy.
One way is to use isActive to manage whether or not a given NavigationLink is presenting its view. That might look like this:
class NavigationReset : ObservableObject {
#Published var rootIsActive = false
func popToTop() {
rootIsActive = false
}
}
struct ContentView: View {
#StateObject private var navReset = NavigationReset()
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "First"), isActive: $navReset.rootIsActive) {
Text("Root nav")
}
}.environmentObject(navReset)
}
}
struct DetailView : View {
var title : String
#EnvironmentObject private var navReset : NavigationReset
var body: some View {
VStack {
NavigationLink(destination: DetailView(title: "\(Date())")) {
Text("Navigate (\(title))")
}
Button("Reset nav") {
navReset.popToTop()
}
}
}
}
Another trick you could use is changing an id on a NavigationLink -- as soon as that happens, it re-renders and becomes inactive.
class NavigationReset : ObservableObject {
#Published var id = UUID()
func popToTop() {
id = UUID()
}
}
struct ContentView: View {
#StateObject private var navReset = NavigationReset()
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "First")) {
Text("Root nav")
}
.id(navReset.id)
}.environmentObject(navReset)
}
}
struct DetailView : View {
var title : String
#EnvironmentObject private var navReset : NavigationReset
var body: some View {
VStack {
NavigationLink(destination: DetailView(title: "\(Date())")) {
Text("Navigate (\(title))")
}
Button("Reset nav") {
navReset.popToTop()
}
}
}
}
It works by marking the first NavigationLink (ie the one on the Home Screen) with an id. As soon as that id is changed, the NavigationLink is recreated, popping all of the views off of the stack.

editMode not working in case of .sheet or .fullScreenCover but with NavigationLink

I'm trying to enable editMode, i.e to come from here:
to here :
with the following code:
struct DetailSheet: View {
#State private var items: [Item] = (0..<5).map { Item(title: "Item #\($0)") }
var body: some View {
NavigationView { // this line to be deleted for using navigationLink
List {
ForEach(items) { item in
Text(item.title)
}
.onMove { (from, to) in
print("just a dummy")
}
} // List
.navigationBarTitle("List")
.navigationBarItems(leading: EditButton())
} // this line to be deleted for using navigationLink
}
}
This works fine, if it is the first view being called from scenedelegate. If I request it from a different "first" view with
.sheet(isPresented: $showDetailSheet, content: {DetailSheet()})
or
.fullScreenCover(isPresented: $showDetailSheet, content: {DetailSheet()})
It does not work anymore. It does work again, if I request the view with a NavigationLink ( if I delete the two lines for NavigationView{ and }.
Am I doing something wrong? If not, can anyone explain me, why editMode doesn't work, if the view was requested by .sheet?
Thanks in advance!
Edit: This is the code of the view (which is called from SceneDelegate) from where I call up DetailView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var showDetailSheet : Bool = false
var body: some View {
NavigationView{
List {
NavigationLink(destination: DetailSheet(), label:{ Text("naviDetailSheet")})
Button(action: {showDetailSheet = true}){Text("DetailSheet")}
} // List
.navigationBarItems(trailing: EditButton())
} // Navi
.sheet(isPresented: $showDetailSheet, content: {DetailSheet()})
// .fullScreenCover(isPresented: $showDetailSheet, content: {DetailSheet()})
}
}

NavigationLink dismisses after TextField changes

I have a navigation stack that's not quite working as desired.
From my main view, I want to switch over to a list view which for the sake of this example represents an array of strings.
I want to then navigate to a detail view, where I want to be able to change the value of the selected string.
I have 2 issues with below code:
on the very first keystroke within the TextField, the detail view is being dismissed
the value itself is not being changed
Also, I suppose there must be a more convenient way to do the binding in the detail view ...
Here's the code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
TestMainView()
}
}
}
struct TestMainView: View {
var body: some View {
NavigationView {
List {
NavigationLink("List View", destination: TestListView())
}
.navigationTitle("Test App")
}
}
}
struct TestListView: View {
#State var strings = [
"Foo",
"Bar",
"Buzz"
]
#State var selectedString: String? = nil
var body: some View {
List(strings.indices) { index in
NavigationLink(
destination: TestDetailView(selectedString: $selectedString),
tag: strings[index],
selection: $selectedString) {
Text(strings[index])
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("List")
}
}
}
struct TestDetailView: View {
#Binding var selectedString: String?
var body: some View {
VStack {
if let _ = selectedString {
TextField("Placeholder",
text: Binding<String>( //what's a better solution here?
get: { selectedString! },
set: { selectedString = $0 }
)
)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Spacer()
}
.navigationTitle("Detail")
}
}
struct TestMainView_Previews: PreviewProvider {
static var previews: some View {
TestMainView()
}
}
I am quite obviously doing it wrong, but I cannot figure out what to do differently...
You're changing the NavigationLink's selection from inside the NavigationLink which forces the TestListView to reload.
You can try the following instead:
struct TestListView: View {
#State var strings = [
"Foo",
"Bar",
"Buzz",
]
var body: some View {
List(strings.indices) { index in
NavigationLink(destination: TestDetailView(selectedString: self.$strings[index])) {
Text(self.strings[index])
}
}
}
}
struct TestDetailView: View {
#Binding var selectedString: String // remove optional
var body: some View {
VStack {
TextField("Placeholder", text: $selectedString)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
}
}

Save selected item in List

This looks like a very simple thing, but I can't figure out how to do this:
I have a List embedded in a NavigationView, containing a NavigationLink to view the detail of the item.
I have a save bar button where I would like to save the selected item. But how can I access the selected item?
It isn't visible in the button's action closure.
struct ItemList : View {
#EnvironmentObject var items: ItemsModel
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: ItemDetail(item: item)) {
Text(item.name)
}
}
.navigationBarTitle(Text("Item"))
.navigationBarItems(trailing: Button(action: {
self.save(/*item: item */) // How can I access item here?
}, label: {
Text("Save")
}))
}
}
func save(item: Item) {
print("Saving...")
}
}
Navigation links are not obligatory to accomplish this.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
var id: Self { self }
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var selectedOceans = [Ocean]()
#State private var multiSelection = Set<Ocean.ID>()
var body: some View {
VStack {
Text("Oceans")
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// Walkaround: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
selectedOceans = Array(multiSelection)
print(selectedOceans)
})
}
Divider()
Text("Selected oceans")
List(selectedOceans, selection: $multiSelection) {
Text($0.name)
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}