Hide chevron/arrow on NavigationLink when displaying a view, SwiftUI - ios

I am trying to remove the chevron that appears on the right of the screen with a navigationLink that contains a view. This is my code below:
NavigationView {
List {
NavigationLink(destination: DynamicList()) {
ResultCard()
}
...
}
Other answers on Stack Overflow have recommended using something like the below:
NavigationLink(...)
.opacity(0)
However, this doesn't work in my case since bringing the opacity down to 0 also removes the view that I am trying to display. This is also the case with '.hidden'. I've searched everywhere and the only somewhat working solution I could find was to alter the padding in order to 'push' the chevron off of the side, but this is a poor solution since the 'ResultCard' view will appear wonky/off-centre on different display sizes.
Perhaps it isn't possible to remove the chevron - and if this is the case, is there any other way I can allow the user to tap the 'ResultCard' view and be taken to a new page, that isn't through a navigation link?
I'm banging my head on the wall so any ideas are very much appreciated.

You can use an .overlay on your label view with a NavigationLink with an EmptyView() set as its label:
struct ContentView : View {
var body: some View {
NavigationView {
List {
NavigationLink("Link 1", destination: Text("Hi"))
Text("Test")
.overlay(NavigationLink(destination: Text("Test"), label: {
EmptyView()
}))
}
}
}
}
Update:
Another solution, which seems to work with other types of Views besides Text:
struct ContentView : View {
#State private var linkActive = false
var body: some View {
NavigationView {
List {
NavigationLink("Link 1", destination: Text("Hi"))
Button(action: { linkActive = true }) {
Image(systemName: "pencil")
}.overlay(VStack {
if linkActive {
NavigationLink(destination: Text("Test"), isActive: $linkActive) {
EmptyView()
}.opacity(0)
}
})
}
}
}
}

The Update solution from jnpdx almost worked for me, but it messed up the animation to the next view. Here's what worked for me (is actually simpler than jnpdx's answer):
struct ContentView : View {
#State private var linkActive = false
var body: some View {
NavigationView {
List {
Button(action: { linkActive = true }) {
Image(systemName: "pencil")
}.overlay(
NavigationLink(
isActive: $linkActive,
destination: { Text("Test") },
label: { EmptyView() }
).opacity(0)
)
}
}
}
}

Here are two alternative variations using .background():
// Replicate the iPhone Favorites tab with the info button
// - Compose a button to link from a NavigationView to a next view
// - Use this when you want to hide the navigation chevron decoration
// - and/or to have a button trigger the link
struct NavigationLinkButton<Destination: View, Label: View>: View {
#Binding var selectedID: String?
#State var rowID: String
#ViewBuilder var destination: () -> Destination
#ViewBuilder var label: () -> Label
var body: some View {
HStack {
Spacer()
Button {
selectedID = rowID
} label: {
label()
}
.buttonStyle(.plain) // prevent List from processing *all* buttons in the row
.background {
NavigationLink("", tag: rowID, selection: $selectedID) {
destination()
}
.hidden()
}
}
}
}
// Replicate the iOS Spotlight search for contacts with action buttons
// - Compose a list row to link from a NavigationView to a next view
// - Use this when you want to hide the navigation chevron decoration
// - and add action buttons
struct NavigationLinkRowHidingChevron<Destination: View, Label: View>: View {
#Binding var selectedID: String?
#State var rowID: String
#ViewBuilder var destination: () -> Destination
#ViewBuilder var label: () -> Label
var body: some View {
ZStack {
// Transparent button to capture taps across the entire row
Button("") {
selectedID = rowID
}
label()
}
.background {
NavigationLink("", tag: rowID, selection: $selectedID) {
destination()
}
.hidden()
}
}
}
// Example Usages
//
// #State private var selectedID: String?
// #State private var editMode: EditMode = .inactive
//
// ... and then in the body:
// List {
// ForEach(items) { item in
// row(for: item)
// .swipeActions(edge: .leading, allowsFullSwipe: true) {
// ...
// }
// .contextMenu {
// ...
// }
// .onDrag({
// ...
// })
// // Overlay so that a tap on the entire row will work except for these buttons
// .overlay {
// // Hide info button just as with Phone Favorites edit button
// if editMode == .inactive {
// NavigationLinkHidingChevron(selectedID: $selectedID, rowID: item.id) {
// // Destination view such as Detail(for: item)
// } label: {
// // Button to activate nav link such as an 􀅴 button
// }
// }
// }
// }
// .onDelete(perform: deleteItems)
// .onMove(perform: moveItems)
// }
//
// ... or this in the body:
// NavigationLinkHidingChevron(selectedID: $selectedID, rowID: contact.id) {
// // Destination view such as Detail(for: item)
// } label: {
// // Content for the list row
// }
// .contextMenu {
// ...
// }
// .overlay {
// HStack(spacing: 15) {
// // Right-justify the buttons
// Spacer()
// // Buttons that replace the chevron and take precedence over the row link
// }
// }

This worked for me, building on #wristbands's solution and targeting iOS 16 using Xcode 14.1:
struct ContentView : View {
var body: some View {
NavigationStack {
List {
Text("View") // your view here
.overlay {
NavigationLink(destination: { Text("Test") },
label: { EmptyView() })
.opacity(0)
}
}
}
}
}

Related

Get the view which was dismissed and from which the current view is display

I have a NavigationView which is showing a different view via fullScreenCover when I press a Button. Now I need to know when my view from where I pressed the Button is getting visible again and what was the view before. In the view shown by the fullScreenCover im using #Environment(\.dismiss) var dismiss to dismiss it.
So here is my concrete use case:
I have my main screen with two Buttons A and B.
A is showing sub view 1 and B is showing sub view 2.
When I dismiss one of these sub screens I need to know if I was in A view 1 or 2 before.
Is this somehow possible?
You can use the .onDisappear on the view that is linked on the destination of the NavigationLink. See this example:
import SwiftUI
struct ContentView: View {
#State var selectedSubView: ViewSelection = ViewSelection.zero
var body: some View {
NavigationView {
VStack {
// Version 1
NavigationLink (
destination: SubView1()
.onDisappear {
self.selectedSubView = .one
print(self.selectedSubView) // debug
},
label: {
Text("To ViewOne").padding()
})
// Version 2
NavigationLink (
destination: SubView2()
.onDisappear {
self.selectedSubView = .two
print(self.selectedSubView) // debug
},
label: {
Text("To ViewTwo")
.padding()
})
}
}
}
}
struct SubView1: View {
var body: some View {
Text("View 1")
}
}
struct SubView2: View {
var body: some View {
Text("View 2")
}
}
enum ViewSelection {
case one
case two
case zero
}
You can use .fullScreenCover with an onDismiss closure.
(Here I used .sheet because it's easier to test, but it's the same)
struct SwiftUIView4: View {
#State private var destination: Destination?
#State private var showed: Destination?
#State private var dismissed: Destination?
var body: some View {
VStack {
HStack {
Button("A") {
destination = .firstView
showed = .firstView
}.padding()
Spacer()
Button("B") {
destination = .secondView
showed = .secondView
}.padding()
}
Text("Just dismissed : \(dismissed?.rawValue ?? "nothing")")
.sheet(item: $destination, onDismiss: {
dismissed = showed
}, content: { destination in
switch destination {
case .firstView: Text("A clicked")
case .secondView: Text("B clicked")
}
})
}
}
}
enum Destination: String, Identifiable {
case firstView, secondView
var id: Destination { self }
}
Or you could use a custom Binding for the item parameter of your .fullScreenCover :
struct SwiftUIView4: View {
#State private var showed: Destination?
#State private var dismissed: Destination?
var body: some View {
VStack {
HStack {
Button("A") {
showed = .firstView
}.padding()
Spacer()
Button("B") {
showed = .secondView
}.padding()
}
Text("Just dismissed : \(dismissed?.rawValue ?? "nothing")")
.sheet(item: .init(get: {showed}, set: {
dismissed = showed // HERE
showed = $0 // <<<<
})) { destination in
switch destination {
case .firstView: Text("A clicked")
case .secondView: Text("B clicked")
}
}
}
}
}

SwiftUI List Button with Disclosure Indicator

I have a SwiftUI view that consists of a list with some items. Some of these are links to other screens (so I use NavigationLink to do this) and others are actions I want to perform on the current screen (E.g. button to show action sheet).
I am looking for a way for a Button in a SwiftUI List to show with a disclosure indicator (the chevron at the right hand sign that is shown for NavigationLink).
Is this possible?
E.g.
struct ExampleView: View {
#State private var showingActionSheet = false
var body: some View {
NavigationView {
List {
NavigationLink("Navigation Link", destination: Text("xx"))
Button("Action Sheet") {
self.showingActionSheet = true
}
.foregroundColor(.black)
}
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
}
Current Behaviour:
Wanted Behaviour:
My answer uses the SwiftUI-Introspect framework, used to:
Introspect underlying UIKit components from SwiftUI
In this case, it is used to deselect the row after the NavigationLink is pressed.
I would think a button with the normal accent color and without the NavigationLink be more intuitive to a user, but if this is what you need, here it is. The following answer should work for you:
import Introspect
import SwiftUI
struct ExampleView: View {
#State private var showingActionSheet = false
#State private var tableView: UITableView?
var body: some View {
NavigationView {
List {
NavigationLink("Navigation Link", destination: Text("xx"))
NavigationLink(
destination: EmptyView(),
isActive: Binding<Bool>(
get: { false },
set: { _ in
showingActionSheet = true
DispatchQueue.main.async {
deselectRows()
}
}
)
) {
Text("Action Sheet")
}
}
.introspectTableView { tableView = $0 }
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
private func deselectRows() {
if let tableView = tableView, let selectedRow = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRow, animated: true)
}
}
}
The possible approach is to make row with custom chevron, like in demo below (tested with Xcode 12.1 / iOS 14.1)
struct ExampleView: View {
#State private var showingActionSheet = false
var body: some View {
NavigationView {
List {
HStack {
Text("Navigation Link")
// need to hide navigation link to use same chevrons
// because default one is different
NavigationLink(destination: Text("xx")) { EmptyView() }
Image(systemName: "chevron.right")
.foregroundColor(Color.gray)
}
HStack {
Button("Action Sheet") {
self.showingActionSheet = true
}
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(Color.gray)
}
}
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
}
I used a ZStack to put a NavigationLink with an empty label behind the actual label content. This way you get the correct chevron symbol.
For the isActive Binding, you can just use a .constant(false) Binding that will always return false and cannot be changed.
This will result in a row that looks exactly like a NavigationLink, but behaves like a Button. This unfortunately also includes the drawback that the user has to click on the label-content (the text) to activate the button and cannot just click on empty space of the cell.
struct ExampleView: View {
#State private var showingActionSheet = false
var body: some View {
NavigationView {
List {
NavigationLink("Navigation Link", destination: Text("xx"))
Button {
self.showingActionSheet = true
} label: {
// Put a NavigationLink behind the actual label for the chevron
ZStack(alignment: .leading) {
// NavigationLink that can never be activated
NavigationLink(
isActive: .constant(false),
destination: { EmptyView() },
label: { EmptyView() }
)
// Actual label content
Text("Action Sheet")
}
}
// Prevent the blue button tint
.buttonStyle(.plain)
}
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
}
struct NavigationButton: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
ZStack(alignment: .leading) {
NavigationLink(
isActive: .constant(false),
destination: { EmptyView() },
label: { EmptyView() }
)
configuration.label
}
}
}
Example:
NavigationView {
List {
Button(action: {
openURL(URL(string: "https://www.apple.com")!)
}) {
Text("Visit Apple")
}
.buttonStyle(NavigationButton())
}
}

Presenting a modal view sheet from a Sub view

I am trying to present a sheet from a sub view selected from the menu item on the navigation bar but the modal Sheet does does not display. I spent a few days trying to debug but could not pin point the problem.
I am sorry, this is a little confusing and will show a simplified version of the code to reproduce. But in a nutshell, the problem seems to be a sheet view that I have as part of the main view. Removing the sheet code from the main view displays the sheet from the sub view. Unfortunately, I don't have the freedom to change the Mainview.swift
Let me show some code to make it easy to understand....
First, before showing the code, the steps to repeat the problem:
click on the circle with 3 dots in the navigation bar
select the second item (Subview)
click on the "Edit Parameters" button and the EditParameters() view will not display
ContentView.swift (just calls the Mainview()). Included code to copy for reproducing issue :-)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Mainview()
}
}
}
}
Mainview.swift. This is a simplified version of the actual App which is quite complex and I don't have leeway to change much here unfortunately!
fileprivate enum CurrentView {
case summary, sub
}
enum OptionSheet: Identifiable {
var id: Self {self}
case add
}
struct Mainview: View {
#State private var currentView: CurrentView? = .summary
#State private var showSheet: OptionSheet? = nil
var body: some View {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
//Removing the below sheet view will display the sheet from the subview but with this sheet here, it the sheet from subview does not work. This is required as these action items are accessed from the second menu item (circle and arrow) navigation baritem
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}
var actionItems: some View {
Menu {
Button(action: {
showSheet = .add
}) {
Label("Add Elements", systemImage: "plus")
}
} label: {
Image(systemName: "cursorarrow.click").resizable()
}
}
var trailingBarItems: some View {
Menu {
Button(action: {currentView = .summary}) {
Label("Summary", systemImage: "list.bullet.rectangle")
}
Button(action: {currentView = .sub}) {
Label("Subview", systemImage: "circle")
}
} label: {
Image(systemName: "ellipsis.circle").resizable()
}
}
#ViewBuilder
func sheetContent(for mode: OptionSheet) -> some View {
switch mode {
case .add:
AddElements()
}
}
#ViewBuilder
var content: some View {
if let currentView = currentView {
switch currentView {
case .summary:
SummaryView()
case .sub:
SubView()
}
}
}
}
Subview.swift. This is the file that contains the button "Edit Parameters" which does not display the sheet. I am trying to display the sheet from this view.
struct SubView: View {
#State private var editParameters: Bool = false
var body: some View {
VStack {
Button(action: {
editParameters.toggle()
}, label: {
HStack {
Image(systemName: "square.and.pencil")
.font(.headline)
Text("Edit Parameters")
.fontWeight(.semibold)
.font(.headline)
}
})
.padding(10)
.foregroundColor(Color.white)
.background(Color(.systemBlue))
.cornerRadius(20)
.sheet(isPresented: $editParameters, content: {
EditParameterView()
})
.padding()
Text("Subview....")
}
.padding()
}
}
EditParameters.swift. This is the view it should display when the Edit Parameters button is pressed
struct EditParameterView: View {
var body: some View {
Text("Edit Parameters...")
}
}
Summaryview.swift. Nothing special here. just including for completeness
struct SummaryView: View {
var body: some View {
Text("Summary View")
}
}
In SwiftUI, you can't have 2 .sheet() modifiers on the same hierarchy. Here, the first .sheet() modifier is on one of the parent views to the second .sheet(). The easy solution is to move one of the .sheets() so it's own hierarchy.
You could either use ZStacks:
var body: some View {
ZStack {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
ZStack{ }
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
}
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}
or more elegantly:
var body: some View {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
.background(
ZStack{ }
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
)
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}

SwiftUI button inactive inside NavigationLink item area

I have a view for a list item that displays some news cards within a navigationLink.
I am supposed to add a like/unlike button within each news card of navigationLink, without being took to NavigationLink.destination page.
It seems like a small button inside a big button.
When you click that small one, execute the small one without executing the bigger one.
(note: the click area is covered by the two buttons, smaller one has the priority)
(In javascript, it seems like something called .stopPropaganda)
This is my code:
var body: some View {
NavigationView {
List {
ForEach(self.newsData.newsList, id:\.self) { articleID in
NavigationLink(destination: NewsDetail(articleID: articleID)) {
HStack {
Text(newsTitle)
Button(action: {
self.news.isBookmarked.toggle()
}) {
if self.news.isBookmarked {
Image(systemName: "bookmark.fill")
} else {
Image(systemName: "bookmark")
}
}
}
}
}
}
}
}
Currently, the button action (like/dislike) will not be performed as whenever the button is pressed, the navigationLink takes you to the destination view.
I have tried this almost same question but it cannot solve this problem.
Is there a way that makes this possible?
Thanks.
as of XCode 12.3, the magic is to add .buttonStyle(PlainButtonStyle()) or BorderlessButtonStyle to the button, when said button is on the same row as a NavigationLink within a List.
Without this particular incantation, the entire list row gets activated when the button is pressed and vice versa (button gets activated when NavigationLink is pressed).
This code does exactly what you want.
struct Artcle {
var text: String
var isBookmarked: Bool = false
}
struct ArticleDetail: View {
var article: Artcle
var body: some View {
Text(article.text)
}
}
struct ArticleCell: View {
var article: Artcle
var toggle: () -> ()
#State var showDetails = false
var body: some View {
HStack {
Text(article.text)
Spacer()
Button(action: {
self.toggle()
}) {
Image(systemName: article.isBookmarked ? "bookmark.fill" : "bookmark").padding()
}
.buttonStyle(BorderlessButtonStyle())
}
.overlay(
NavigationLink(destination: ArticleDetail(article: article), isActive: $showDetails) { EmptyView() }
)
.onTapGesture {
self.showDetails = true
}
}
}
struct ContentView: View {
#State var articles: [Artcle]
init() {
_articles = State(initialValue: (0...10).map { Artcle(text: "Article \($0 + 1)") })
}
func toggleArticle(at index: Int) {
articles[index].isBookmarked.toggle()
}
var body: some View {
NavigationView {
List {
ForEach(Array(self.articles.enumerated()), id:\.offset) { offset, article in
ArticleCell(article: article) {
self.toggleArticle(at: offset)
}
}
}
}
}
}

How to display missing back button in Master-Detail1-Detail2 view in SwiftUI in landscape?

I'm using a NavigationView and NavigationLinks to move from a MasterView to a Detail1View and then further to a Detail2View.
On an iPhone in portrait mode, the back button is displayed for all detail views and allows to move back from Detail2View to Detail1View, and then further to MasterView.
But on an iPhone in landscape mode or on an iPad, when moving from Detail1View to Detail2View, there is no back button. And it is thus impossible to go back to Detail1View.
Adding a 2nd NavigationView allows to have a back button, but it's not really a desirable workaround, as the 2nd UINavigationViewController is shown below the 1st one.
struct MyMasterView: View {
private let names = ["Homer", "Marge", "Bart", "Lisa"]
var body: some View {
List() {
Section(header: Text("The Simpsons")) {
ForEach(names, id: \.self) { name in
NavigationLink(destination: MyDetail1View(name: name)) {
Text(name)
}
}
}
}
.navigationBarTitle(Text("My App"))
}
}
struct MyDetail1View: View {
var name: String
var body: some View {
//NavigationView {
List {
NavigationLink(destination: MyDetail2View(name: name)) {
Text("Hello \(name)")
}
}
.navigationBarTitle(Text("Details 1"))
//}
}
}
struct MyDetail2View: View {
var name: String
var body: some View {
HStack {
Text("\(name), I'm sorry but in Landscape there is no back button...")
}
.navigationBarTitle(Text("Details 2"))
}
}
struct ContentView : View {
var body: some View {
NavigationView {
MyMasterView()
Text("In Landscape, swipe to start...")
}
}
}
The answer turns out to be as simple as using isDetailLink()!
NavigationLink(destination: MyDetail2View(name: name)) {
Text("Hello \(name)")
}.isDetailLink(false)
Credits to https://stackoverflow.com/a/57400873/2893408

Resources