SwiftUI Multiple Labels Vertically Aligned - swiftui

There are a lot of solutions for trying to align multiple images and text in SwiftUI using a HStacks inside of a VStack. Is there any way to do it for multiple Labels? When added in a list, multiple labels automatically align vertically neatly. Is there a simple way to do this for when they are embedded inside of a VStack?
struct ContentView: View {
var body: some View {
// List{
VStack(alignment: .leading){
Label("People", systemImage: "person.3")
Label("Star", systemImage: "star")
Label("This is a plane", systemImage: "airplane")
}
}
}

So, you want this:
We're going to implement a container view called EqualIconWidthDomain so that we can draw the image shown above with this code:
struct ContentView: View {
var body: some View {
EqualIconWidthDomain {
VStack(alignment: .leading) {
Label("People", systemImage: "person.3")
Label("Star", systemImage: "star")
Label("This is a plane", systemImage: "airplane")
}
}
}
}
You can find all the code in this gist.
To solve this problem, we need to measure each icon's width, and apply a frame to each icon, using the maximum of the widths.
SwiftUI provides a system called “preferences” by which a view can pass a value up to its ancestors, and the ancestors can aggregate those values. To use it, we create a type conforming to PreferenceKey, like this:
fileprivate struct IconWidthKey: PreferenceKey {
static var defaultValue: CGFloat? { nil }
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
switch (value, nextValue()) {
case (nil, let next): value = next
case (_, nil): break
case (.some(let current), .some(let next)): value = max(current, next)
}
}
}
To pass the maximum width back down to the labels, we'll use the “environment” system. For that, we need an EnvironmentKey. In this case, we can use IconWidthKey again. We also need to add a computed property to EnvironmentValues that uses the key type:
extension IconWidthKey: EnvironmentKey { }
extension EnvironmentValues {
fileprivate var iconWidth: CGFloat? {
get { self[IconWidthKey.self] }
set { self[IconWidthKey.self] = newValue }
}
}
Now we need a way to measure an icon's width, store it in the preference, and apply the environment's width to the icon. We'll create a ViewModifier to do those steps:
fileprivate struct IconWidthModifier: ViewModifier {
#Environment(\.iconWidth) var width
func body(content: Content) -> some View {
content
.background(GeometryReader { proxy in
Color.clear
.preference(key: IconWidthKey.self, value: proxy.size.width)
})
.frame(width: width)
}
}
To apply the modifier to the icon of each label, we need a LabelStyle:
struct EqualIconWidthLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon.modifier(IconWidthModifier())
configuration.title
}
}
}
Finally, we can write the EqualIconWidthDomain container. It needs to receive the preference value from SwiftUI and put it into the environment of its descendants. It also needs to apply the EqualIconWidthLabelStyle to its descendants.
struct EqualIconWidthDomain<Content: View>: View {
let content: Content
#State var iconWidth: CGFloat? = nil
init(#ViewBuilder _ content: () -> Content) {
self.content = content()
}
var body: some View {
content
.environment(\.iconWidth, iconWidth)
.onPreferenceChange(IconWidthKey.self) { self.iconWidth = $0 }
.labelStyle(EqualIconWidthLabelStyle())
}
}
Note that EqualIconWidthDomain doesn't just have to be a VStack of Labels, and the icons don't have to be SF Symbols images. For example, we can show this:
Notice that one of the label “icons” is an emoji in a Text. All four icons are laid out with the same width (across both columns). Here's the code:
struct FancyView: View {
var body: some View {
EqualIconWidthDomain {
VStack {
Text("Le Menu")
.font(.caption)
Divider()
HStack {
VStack(alignment: .leading) {
Label(
title: { Text("Strawberry") },
icon: { Text("🍓") })
Label("Money", systemImage: "banknote")
}
VStack(alignment: .leading) {
Label("People", systemImage: "person.3")
Label("Star", systemImage: "star")
}
}
}
}
}
}

This has been driving me crazy myself for a while. One of those things where I kept approaching it the same incorrect way - by seeing it as some sort of alignment configuration that was inside the black box that is List.
However it appears that it is much simpler. Within the List, Apple is simply applying a ListStyle - seemingly one that is not public.
I created something that does a pretty decent job like this:
public struct ListLabelStyle: LabelStyle {
#ScaledMetric var padding: CGFloat = 6
public func makeBody(configuration: Configuration) -> some View {
HStack {
Image(systemName: "rectangle")
.hidden()
.padding(padding)
.overlay(
configuration.icon
.foregroundColor(.accentColor)
)
configuration.title
}
}
}
This uses a hidden rectangle SFSymbol to set the base size of the icon. This is not the widest possible icon, however visually it seems to work well. In the sample below, you can see that Apple's own ListStyle assumes that the label icon will not be something significantly larger than the SFSymbol with the font being used.
While the sample here is not pixel perfect with Apple's own List, it's close and with some tweaking, you should be able to achieve what you are after.
By the way, this works with dynamic type as well.
Here is the complete code I used to generate this sample.
public struct ListLabelStyle: LabelStyle {
#ScaledMetric var padding: CGFloat = 6
public func makeBody(configuration: Configuration) -> some View {
HStack {
Image(systemName: "rectangle")
.hidden()
.padding(padding)
.overlay(
configuration.icon
.foregroundColor(.accentColor)
)
configuration.title
}
}
}
struct ContentView: View {
#ScaledMetric var rowHeightPadding: CGFloat = 6
var body: some View {
VStack {
Text("Lazy VStack Plain").font(.title2)
LazyVStack(alignment: .leading) {
ListItem.all
}
Text("Lazy VStack with LabelStyle").font(.title2)
LazyVStack(alignment: .leading, spacing: 0) {
vStackContent
}
.labelStyle(ListLabelStyle())
Text("Built in List").font(.title2)
List {
ListItem.all
labelWithHugeIcon
labelWithCircle
}
.listStyle(PlainListStyle())
}
}
// MARK: List Content
#ViewBuilder
var vStackContent: some View {
ForEach(ListItem.allCases, id: \.rawValue) { item in
vStackRow {
item.label
}
}
vStackRow { labelWithHugeIcon }
vStackRow { labelWithCircle }
}
func vStackRow<Content>(#ViewBuilder _ content: () -> Content) -> some View where Content : View {
VStack(alignment: .leading, spacing: 0) {
content()
.padding(.vertical, rowHeightPadding)
Divider()
}
.padding(.leading)
}
// MARK: List Content
var labelWithHugeIcon: some View {
Label {
Text("This is HUGE")
} icon: {
HStack {
Image(systemName: "person.3")
Image(systemName: "arrow.forward")
}
}
}
var labelWithCircle: some View {
Label {
Text("Circle")
} icon: {
Circle()
}
}
enum ListItem: String, CaseIterable {
case airplane
case people = "person.3"
case rectangle
case chevron = "chevron.compact.right"
var label: some View {
Label(self.rawValue, systemImage: self.rawValue)
}
static var all: some View {
ForEach(Self.allCases, id: \.rawValue) { item in
item.label
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
// .environment(\.sizeCategory, .extraExtraLarge)
}
}

Combining a few of these answers into another simple option (Very similar to some of the other options but thought it was distinct enough that some may find it useful). This has the simplicity of just setting a frame on the icon, and the swiftUI-ness of using LabelStyle but still adapts to dynamic type!
struct StandardizedIconWidthLabelStyle: LabelStyle {
#ScaledMetric private var size: CGFloat = 25
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.frame(width: size, height: size)
}
}
}

The problem is that the system icons have different standard widths. It's probably easiest to use an HStack as you mentioned. However, if you use the full Label completion, you'll see that the Title is actually just a Text and the icon is just an Image... and you can then add custom modifiers, such as a specific frame for the image width. Personally, I'd rather just use an HStack anyway.
var body: some View {
VStack(alignment: .leading){
Label(
title: {
Text("People")
},
icon: {
Image(systemName: "person.3")
.frame(width: 30)
})
Label(
title: {
Text("Star")
},
icon: {
Image(systemName: "star")
.frame(width: 30)
})
Label(
title: {
Text("This is a plane")
},
icon: {
Image(systemName: "airplane")
.frame(width: 30)
})
}
}

Related

How to recreate the grid (in screenshot) in SwiftUI without breaking navigation?

I am trying to recreate a layout similar to the Reminders app. Looking at it makes me think it was built with SwiftUI. I also believe Apple mentioned so in one of the WWDC videos (can't remember which one).
This above screenshot seems to be a List, with a LazyVGrid as the first View inside the List. Tapping on each of the items in the LazyVGrid, such as Today, Scheduled, All and Flagged, navigates to the relevant screen, which means they are all NavigationLinks. Also note that the LazyVGrid has 2 columns.
And then there is another section "My Lists" which has rows which look like regular list rows in a List with style .insetGrouped. Also, every item in this Section is a NavigationItem, and thus comes with the disclosure indicator on the right as usual. Recreating this is trivial, so it has been left out from the MRE.
I am having trouble recreating the first section, which has that LazyVGrid. I faced 3 problems (as mentioned in the image), of which I have been able to solve the first one only. The other two problems remain. I want to know if this MRE can be fixed, or is my entire approach incorrect.
I am including a minimum reproducible example below.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
RemindersView()
}
}
}
struct RemindersView: View {
private var columns: [GridItem] = [GridItem(.adaptive(minimum: 150))]
private var smartLists: [SmartList] = SmartList.sampleLists
var body: some View {
NavigationView {
List {
Section(header: Text("Using LazyVGrid")) {
grid
}
Section(header: Text("Using HStack")) {
hstack
}
}
.navigationTitle("Store")
}
.preferredColorScheme(.dark)
}
private var grid: some View {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(smartLists) { smartList in
// This use of **ZStack with an EmptyView with opacity 0** is a hack being used to avoid the disclosure indicator on each item in the grid
ZStack(alignment: .leading) {
NavigationLink( destination: SmartListView(list: smartList)) {
EmptyView()
}
.opacity(0)
SmartListView(list: smartList)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
private var hstack: some View {
ScrollView(.horizontal) {
HStack {
ForEach(smartLists) { smartList in
NavigationLink(destination: SmartListView(list: smartList)) {
SmartListView(list: smartList)
}
.buttonStyle(.plain)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
}
struct RemindersView_Previews: PreviewProvider {
static var previews: some View {
RemindersView()
}
}
struct SmartList: Identifiable {
var id: UUID = UUID()
var title: String
var count: Int
var icon: String
var iconColor: Color
static var sampleLists: [SmartList] {
let today = SmartList(title: "Today", count: 5, icon: "20.circle.fill", iconColor: .blue)
let scheduled = SmartList(title: "Scheduled", count: 12, icon: "calendar.circle.fill", iconColor: .red)
let all = SmartList(title: "All", count: 77, icon: "tray.circle.fill", iconColor: .gray)
let flagged = SmartList(title: "Flagged", count: 5, icon: "flag.circle.fill", iconColor: .orange)
return [today, scheduled, all, flagged]
}
}
struct SmartListView: View {
var list: SmartList
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .center) {
Image(systemName: list.icon)
.renderingMode(.original)
.font(.title)
.foregroundColor(list.iconColor)
Spacer()
Text("\(list.count)")
.font(.system(.title, design: .rounded))
.fontWeight(.bold)
.padding(.horizontal, 8)
}
Text(list.title)
.font(.system(.headline, design: .rounded))
.foregroundColor(.secondary)
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.gray.opacity(0.25))
)
.padding(2)
.frame(minWidth: 150)
}
}
EDIT 1: Adding video demo of what editing the dynamic Grid looks like and how the Grid has dynamic grid items (via the Edit button at the top right): https://imgur.com/a/TV0kifY

How to layout properly in ZStack (I have visibility problem)?

Here is reproducable small code below;
As you'll see when you run the demo code, the Element view does stay under Color.blue when dragged eventhough its above according to ZStack. By the way I also played with zIndex modifier but still no luck. Any solution you offer? Thanks all.
struct ContentView: View {
var body: some View {
GeometryReader { gr in
ZStack {
Color.blue.opacity(0.3)
.aspectRatio(1, contentMode: .fit)
.frame(width: gr.size.width)
VStack {
Spacer()
ScrollView(.horizontal) {
HStack {
ForEach(1...15, id: \.self) { (idx) in
Element(index: idx)
}
}
.padding()
}
.background(Color.secondary.opacity(0.3))
}
}
}
}
}
struct Element: View {
#State private var dragAmount = CGSize.zero
var index: Int
var body: some View {
Rectangle()
.frame(width: 80, height: 80)
.overlay(Text("\(index)").bold().foregroundColor(.white))
.offset(dragAmount)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmount = CGSize(width: $0.translation.width, height: $0.translation.height)
}
.onEnded { _ in
self.dragAmount = .zero
}
)
}
}
iOS 15.5: still valid
How can achieve my goal then, like dragging Element on different view (in this scenario Color.blue)
Actually we need to disable clipping by ScrollView.
Below is possible approach based on helper extensions from my other answers (https://stackoverflow.com/a/63322713/12299030 and https://stackoverflow.com/a/60855853/12299030)
VStack {
Spacer()
ScrollView(.horizontal) {
HStack {
ForEach(1...15, id: \.self) { (idx) in
Element(index: idx)
}
}
.padding()
.background(ScrollViewConfigurator {
$0?.clipsToBounds = false // << here !!
})
}
.background(Color.secondary.opacity(0.3))
}

How to create a custom delete button without using the slide to delete that comes with swiftUI I am not using list, just using a foreach loop

Since, the onDelete and onMove are features of List/form I cannot use them when I have custom interfaces without them. I have used a VStack inside a ForEach. I am quite new to swiftUI and unsure on how I can implement custom code for onDelete and onMove.
Here's my code:
struct Trying: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
numbers.remove(atOffsets: offsets)
}
}
The way it works right now:
Here is a simple demo of possible approach to implement custom delete (of course with move it would be more complicated due to drag/drop, but idea is the same). Tested with Xcode 12 / iOS 14.
struct DemoCustomDelete: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
.overlay(
DeleteButton(number: number, numbers: $numbers, onDelete: removeRows)
, alignment: .topTrailing)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
withAnimation {
numbers.remove(atOffsets: offsets)
}
}
}
struct DeleteButton: View {
#Environment(\.editMode) var editMode
let number: Int
#Binding var numbers: [Int]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
Based on #Asperi's answer, I just generalized it to accept any Equatable sequence.
struct DeleteButton<T>: View where T: Equatable {
#Environment(\.editMode) var editMode
let number: T
#Binding var numbers: [T]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
I recently had the need to delete a row and I couldn't use a LIST. Instead I had a scroll view... But I was able to implement the edit to simulate the same onDelete behavior as if it was a list. Initially I couldn't get my code to work. It wasn't until I closely examined my implementation and experimented that I stumbled on why mine worked. I'm coding for an iPad so my NavigationView uses,
.navigationViewStyle(StackNavigationViewStyle())
Once I added this to the struct's NavigationView, when you click on the EditButton it activates editMode?.wrappedValue to .active / .inactive
Below is my implementation for the code sample above...
struct Trying: View {
#State var num: Int = 0
#Environment(\.editMode) var editMode
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack {
ForEach(numbers, id: \.self) { number in
HStack {
if editMode?.wrappedValue == .active {
Button(action: { num = number
removeRows(numbr: num)
}, label: {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
})
} // END IF editMode?wrappedValue == .active
Text("\(number)")
.frame(width: 50, height: 50)
.background(Color.red)
}
}
// .onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
// FOR SOME REASON THIS ALLOWS THE EditButton() to activate editMode without a LIST being present.
.navigationViewStyle(StackNavigationViewStyle())
}
func removeRows(numbr: Int) {
print("removing \(numbr)")
}
}
It looks like:

Is it possible to have multiple NavigationLinks per row in a List SwiftUI?

I can't use multiple NavigationLinks in the same row of a List.
It looks like the navigation stack is totally messed up, because you tap once and it goes to multiple views and back erratically...
in TestList, I've tried adding the separate NavigationLinks in Sections, and I've tried moving the NavigationLinks two different places in the view hierarchy...
I've tried adding two NavigationViews for each row of the list, but
then the navigationTitleBar don't go away when I need it to..
struct ContentView: View {
var body: some View {
NavigationView {
TestList()
}
}
}
struct TestList: View {
var body: some View {
List {
ListCellView()
}
}
}
struct ListCellView: View {
var body: some View {
VStack {
Spacer()
NavigationLink(destination: TestDestination1()) {
Text("Test Destination 1")
.frame(width: 140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.green, lineWidth: 3.0))
}
Spacer()
NavigationLink(destination: TestDestination2()) {
Text("Test Destination 2")
.frame(width:140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.purple, lineWidth: 3.0))
Spacer()
}
}
}
}
struct TestDestination1: View {
var body: some View {
Text("Test Destination 1")
}
}
struct TestDestination2: View {
var body: some View {
Text("Test Destination 2")
}
}
I expect that when you tap a NavigationLink, it will navigate to the destination view.
What happens is when two NavigationLinks are in the same row of a List and you tap in it, it will:
1. go to one of the views
2. After tapping 'back', it will take you back to the view AND THEN take you to the other destination view.
https://youtu.be/NCTnqjzJ4VE
As the others mentioned, why 2 NavigationLinks in 1 cell. The issue is with multiple buttons and gesture in general for the Cell. I guess it is expected 1 Button/NavigationLink max per cell. As you noticed, on your video, you tap on a NavigationLink but your full Cell got the gesture (highlighted), which in return impact the other Buttons/NavigationLinks.
Anyhow, you can have it working, the 2 NavigationLinks in 1 cell, with a hack. Below I have created SGNavigationLink, which I use for my own app which solve your issue. It simply replace NavigationLink and based on TapGesture, so you will lose the highlight.
NB: I slightly modified your ListCellView, as the Spacer within my SGNavigationLink was creating an internal crash.
struct ListCellView: View {
var body: some View {
VStack {
HStack{
SGNavigationLink(destination: TestDestination1()) {
Text("Test Destination 1")
.frame(width: 140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.green, lineWidth: 3.0))
}
Spacer()
}
HStack{
SGNavigationLink(destination: TestDestination2()) {
Text("Test Destination 2")
.frame(width:140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.purple, lineWidth: 3.0))
}
Spacer()
}
}
}
}
struct SGNavigationLink<Content, Destination>: View where Destination: View, Content: View {
let destination:Destination?
let content: () -> Content
#State private var isLinkActive:Bool = false
init(destination: Destination, title: String = "", #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.destination = destination
}
var body: some View {
return ZStack (alignment: .leading){
if self.isLinkActive{
NavigationLink(destination: destination, isActive: $isLinkActive){Color.clear}.frame(height:0)
}
content()
}
.onTapGesture {
self.pushHiddenNavLink()
}
}
func pushHiddenNavLink(){
self.isLinkActive = true
}
}
I am not sure about why do you need multiple Navigationlinks (duplicate code). You can use a data source that will hold the required properties of the list [title, color, id etc] and based on the id, call the desired View. Reuse the same code. Here is an example.
struct TestList: View {
var body: some View {
List { // <- Use Data source
ForEach(0..<2) { index in
ListCellView(index: index)
}
}
}
}
struct ListCellView: View {
var index: Int
var body: some View {
return NavigationLink(destination: ViewFactory.create(index)) {
Text("Test Destination 1")
.frame(width: 140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.green, lineWidth: 3.0))
}
}
}
class ViewFactory {
static func create(_ index: Int) -> AnyView {
switch index {
case 0:
return AnyView(TestDestination1())
case 1:
return AnyView(TestDestination2())
default:
return AnyView(EmptyView())
}
}
}
struct TestDestination1: View {
var body: some View {
Text("Test Destination 1")
}
}
struct TestDestination2: View {
var body: some View {
Text("Test Destination 2")
}
}

SwiftUI NavigationButton without the disclosure indicator?

When making a List with a row that pushes to a new view, SwiftUI adds a disclosure indicator ">" automatically? How do I remove it if I don't want it?
NavigationView {
List {
NavigationButton(destination: DetailView()) {
ListItem()
}
}
.navigationBarTitle(Text("Some title"))
}
On a UITableViewCell you set Accessory to None but how do I do that in SwiftUI?
Setting the NavigationLink width and hiding it did the trick for me
List {
ForEach(pages) { page in
HStack(spacing: 0) {
Text("Something")
NavigationLink(destination: Text("Somewhere")) {
EmptyView()
}
.frame(width: 0)
.opacity(0)
}
}
}
Swift 5, Xcode 11. ZStack works perfect.
var body: some View {
NavigationView {
List {
ForEach(viewModel.currenciesViewModel) { cellViewModel in
ZStack {
cellViewModel.makeView()
NavigationLink(destination: ChooseCurrencyListView()) {
EmptyView()
}
.buttonStyle(PlainButtonStyle())
}
}
}
.navigationBarHidden(true)
.navigationBarTitle("", displayMode: .inline)
}
}
The easiest one. The content for each item in the list.
ZStack {
NavigationLink(destination: DetailView()) {
EmptyView()
}.hidden()
RowView()
}
As workaround I can suggest to add .padding modifier like this:
NavigationView {
List {
NavigationButton(destination: DetailView()) {
ListItem()
}
}
.navigationBarTitle(Text("Some title"))
}
.padding(.trailing, -32.0)
So you will get rows without visible disclosure:
You can also put it in the .background modifier:
List {
Text("Go to...")
.background(NavigationLink("", destination: Text("Detail View")))
}
If you already have the background modifier on the Text, you can wrap the Text in a HStack and apply background to the HStack.
What you can do, if you are using list, is setting the navigationlink to hidden and its frame width to zero.
HStack{
Button(action: {self.statusShow = 1}, label: {
Image(systemName: "info.circle")
})
NavigationLink(destination: StimulatorSettingView(),
tag: 1,
selection: self.$statusShow){
EmptyView()
}.hidden().frame(width: 0)
}
This worked for me.
As of beta 6, this works well:
struct SwiftUIView: View {
var body: some View {
NavigationView {
List {
HStack {
Text("My Cell Content")
NavigationLink(destination: Text("destination"), label: {
EmptyView()
})
}
}
}
}
}
You don't have to use NavigationLink to wrap your Label directly. It will work as long as the link is anywhere in your view hierarchy.
Here I've wrapped it in a button, which allows you to trigger an action prior to pushing the view. Since the NavigationLink has an EmptyView for the label the disclosure indicator is not visible. You can also style this with ButtonStyle.
struct NavigationButton<Destination: View, Label: View>: View {
var action: () -> Void = { }
var destination: () -> Destination
var label: () -> Label
#State private var isActive: Bool = false
var body: some View {
Button(action: {
self.action()
self.isActive.toggle()
}) {
self.label()
.background(NavigationLink(destination: self.destination(), isActive: self.$isActive) {
EmptyView()
})
}
}
}
And to use it:
NavigationButton(
action: { print("tapped!") },
destination: { Text("Pushed View") },
label: { Text("Tap me") }
)
NavigationLink is what we should define in a scope enclosed inside a NavigationView.
But when we use NavigationLink it is attached to the enclosing view, so to reuse the same NavigationLink with other views, we use tag which differentiates between different Destinations.
struct SwiftUIView: View {
#State private var viewState: Int? = 0
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("View 1"), tag: 1, selection: $viewState) {
EmptyView()
}
NavigationLink(destination: Text("View 2"), tag: 2, selection: $viewState) {
EmptyView()
}
Text("First View")
.onTapGesture {
self.viewState = 1
}
Text("Second View")
.onTapGesture {
self.viewState = 2
}
}
}
}
}
Here we bind a Hashable property with all the NavigationLinks present in our VStack so that when a particular View is tapped we can notify which Destination should be opened by setting the value of Bindable property.
If we don't notify the correct Destination by setting the value of tag, always the View defined inside the Closure of NavigationLink will be clickable and nothing else.
Using this approach you don't need to wrap all your clickable views inside NavigationView, any action on any view can use any NavigationLink just by setting the tag.
Thanks, hope this helps.
Works well for me!
import SwiftUI
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
EmptyView()
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone 11 Pro Max"], id: \.self) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
}
}
Use .frame(width: 0).opacity(0.0):
NavigationView {
List {
ForEach(options) {
option in
ZStack {
YourView(option: option)
NavigationLink(destination: ProductListView(),
label: {
EmptyView()
}).frame(width: 0).opacity(0.0)
}.listRowInsets(EdgeInsets())
}
}.navigationBarHidden(true)
}
My version of this solution is to make a view modifier. I think it's the cleanest way, as it doesn't use AnyView.
Note that this solution runs the init() for the destination when it draws the element the .navigationLink() is attached to.
Usage
Text("Link")
.navigationLink({
// put your destination here
})
How To
import SwiftUI
extension View {
func navigationLink<Destination: View>(_ destination: #escaping () -> Destination) -> some View {
modifier(NavigationLinkModifier(destination: destination))
}
}
fileprivate struct NavigationLinkModifier<Destination: View>: ViewModifier {
#ViewBuilder var destination: () -> Destination
func body(content: Content) -> some View {
content
.background(
NavigationLink(destination: self.destination) { EmptyView() }.opacity(0)
)
}
}
This helps to push and pass the model to the next navigation view controller.
struct ContentView : View {
#State var model = PostListViewModel()
var body: some View {
NavigationView {
List(model.post) { post in
ListCell(listData: post)
}.navigationBarTitle(Text("My Post"))
}
}
}
struct ListCell: View {
var listData: Post
var body: some View {
return NavigationButton(destination: DetailContentView(post: listData)) {
HStack {
ImageRow(model: listData) // Get image
VStack(alignment: .leading) {
Text(listData.login).font(.headline).lineLimit(nil)
Text(listData.url).font(.subheadline).lineLimit(nil)
}.padding(.leading, 10)
}.padding(.init(top: 5, leading: 0, bottom: 5, trailing: 0))
}
}
}
Here's a reusable "plain" navigation link view (i.e. without the chevron disclosure indicator) that can be a drop-in replacement for NavigationLink:
struct PlainNavigationLink<Label, Destination>: View where Label: View, Destination: View {
#ViewBuilder var destination: () -> Destination
#ViewBuilder var label: () -> Label
var body: some View {
label()
.background(
NavigationLink(destination: destination, label: {})
.opacity(0)
)
}
}
To use it, simply replace NavigationLink with PlainNavigationLink:
NavigationView { // or NavigationStack in iOS 16
List {
ForEach(1...30, id: \.self) { _ in
PlainNavigationLink {
Text("Hello, world!")
} label: {
Text("Hello, world!")
}
}
}
}
We can also extend it with convenience initializers for LocalizedStringKey and String, just like NavigationLink does.
just came here looking for the answer to this question, but none of the proposed solutions worked for me (can't have an empty view, because i want to put something in the list row; i'm already messing with the padding (and increasing trailing padding didn't seem to work) ... i was about to give up, and then something occurred to me: what if you crank up the z-index of the list row itself? seemed somewhat unlikely, but i gave it a try and, i'll be damned, it worked! i was so pleasantly surprised, i felt like sharing ...
e.g.:
// in body of your list row view
HStack(alignment: .top, spacing: 0.0) {
// stuff ...
}
.zIndex(9999999999)
If you need children behaviour for List and NavigationLink, without additional discloser in the same time, I want to promote this tricky solution, main point at HStack
var body: some View {
NavigationView {
List(items, children: \.items) { item in
ZStack {
NavigationLink(destination: DetailsView()) {
EmptyView()
}.hidden()
HStack {
RowView(item: item)
Spacer()
}
}
}
}
}
Once you put your button in a scrollview, the disclosure button will be hidden. Just make sure to disable your scroll indicator.
there is no documentation yet, so you can use ScrollView for now
NavigationView {
ScrollView {
ForEach(0...100){ x in
NavigationButton(destination: Text("ss")) {
HStack {
Text(String(x))
Spacer()
}
.padding()
.background(Color.white)
.shadow(radius:1,y:1)
}
}
.frame(width: UIScreen.main.bounds.width - 32)
.padding()
}
}
Removing List and just using ForEach works fine with navigation link. You just have to create your own list row. This works for me
NavigationView {
ForEach(pages) {
page in
NavigationLink(destination: DetailView()) {
ListItem()
}
}
}