23

I am searching for a solution to show the disclosure indicator chevron without having the need to wrap my view into an NavigationLink. For example I want to show the indicator but not navigate to a new view but instead show a modal for example.

I have found a lot solutions that hide the indicator button but none which explains how to add one. Is this even possible in the current SwiftUI version ?

struct MyList: View {
    var body: some View {
        NavigationView {
        List {
            Section {
                Text("Item 1")
                Text("Item 2")
                Text("Item 3")
                Text("Item 4")

            }
        }
    }
}

For example I want to add the disclosure indicator to Item 1 without needing to wrap it into an NavigationLink

I already tried to fake the indicator with the chevron.right SF Symbol, but the symbol does not match 100% the default iOS one. Top is default bottom is chevron.right.

Disclosure Button Image

grahan
  • 2,148
  • 5
  • 29
  • 43
  • 3
    This looks similar: `Image(systemName: "chevron.right").font(Font.system(.footnote).weight(.semibold))` – pawello2222 Jun 06 '20 at 23:13
  • @pawello2222 Thanks a lot, also looks pretty the same as the original. Now I have two solutions (y) – grahan Jun 06 '20 at 23:24

9 Answers9

17

It is definitely possible.

You can use a combination of Button and a non-functional NavigationLink to achieve what you want.

Add the following extension on NavigationLink.

extension NavigationLink where Label == EmptyView, Destination == EmptyView {

   /// Useful in cases where a `NavigationLink` is needed but there should not be
   /// a destination. e.g. for programmatic navigation.
   static var empty: NavigationLink {
       self.init(destination: EmptyView(), label: { EmptyView() })
   }
}

Then, in your List, you can do something like this for the row:

// ...
ForEach(section.items) { item in
    Button(action: {
        // your custom navigation / action goes here
    }) {
        HStack {
            Text(item.name)
            Spacer()
            NavigationLink.empty
        }
    }
 }
 // ...

The above produces the same result as if you had used a NavigationLink and also highlights / dehighlights the row as expected on interactions.

George Marmaridis
  • 1,814
  • 1
  • 13
  • 15
  • Thanks a lot, I prefer this solution to the chevron symbol. Only drawback is that this only works on iOS 14, it doesn't work on iOS 13. Also, need to set font color for Text otherwise it will be shown in blue. – xiang Nov 20 '21 at 06:40
  • (`Spacer` is not necessary inside `HStack` since `Text` is the only visible view anyway.) – lazarevzubov May 19 '23 at 10:05
  • 1
    Adding the button modifier `.buttonStyle(.plain)` will remove the blue tint and correct the colour of the disclosure chevron. – OxfordSi Jun 05 '23 at 07:35
  • this is not correct because the arrow will be tappable and it will navigate to an empty view. – rsergiu2003 Aug 10 '23 at 14:08
12

Hopefully, this is what you are looking for. You can add the item to a HStack and with a Spacer in between fake it that its a Link:

HStack {
                    Text("Item 1")
                    Spacer()
                    Button(action: {

                    }){
                        Image(systemName: "chevron.right")
                            .font(.body)
                    }
                }
Geart Otten
  • 241
  • 4
  • 11
  • Thanks a lot for your answer! - I already did that and unfortunately it did not solve my problem 100% because the `chevron.right` does not match the appearance for the default disclosure indicator. I should have written that in my question, sorry for that -> edited my question now. – grahan Jun 06 '20 at 23:06
  • 2
    You could try to heavily edit the font into the right style. I got pretty close by doing: .font(Font.system(size: 13, weight: .semibold, design: .default)) .foregroundColor(Color(red: 0.771, green: 0.771, blue: 0.779)) – Geart Otten Jun 06 '20 at 23:15
  • 1
    Oh you are right, I totally forgot that I could modify the image in that way. So many things to remember in SwiftUI. Thanks, it looks really like the original. Thanks ! – grahan Jun 06 '20 at 23:23
  • 8
    If you use the color `.foregroundColor(Color(UIColor.tertiaryLabel))` then it correctly supports dark mode as well. – Bart van Kuik Nov 17 '20 at 10:12
11

The answers already submitted don't account for one thing: the highlighting of the cell when it is tapped. See the About Peek-a-View cell in the image at the bottom of my answer — it is being highlighted because I was pressing it when the screenshot was taken.

My solution accounts for both this and the chevron:

Button(action: { /* handle the tap here */ }) {
    NavigationLink("Cell title", destination: EmptyView())
}
.foregroundColor(Color(uiColor: .label))

The presence of the Button seems to inform SwiftUI when the cell is being tapped; simply adding an onTapGesture() is not enough.

The only downside to this approach is that specifying the .foregroundColor() is required; without it, the button text will be blue instead.

Screenshot of a SwiftUI List with one cell highlighted

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
cliss
  • 626
  • 7
  • 8
7

in iOS15 the following is a better match as the other solutions were little too big and not bold enough. it'll also resize better to different Display scales better than specifying font sizes.

HStack {
    Text("Label")
    Spacer()
    Image(systemName: "chevron.forward")
      .font(Font.system(.caption).weight(.bold))
      .foregroundColor(Color(UIColor.tertiaryLabel))
}

Would be good if there was an offical way of doing this. Updating every OS tweak is annoying.

ngb
  • 821
  • 9
  • 21
2

I found an original looking solution. Inserting the icon by hand does not bring the exact same look.

The trick is to use the initializer with the "isActive" parameter and pass a local binding which is always false. So the NavigationLink waits for a programmatically trigger event which will never occur.

// use this initializer
NavigationLink(isActive: <Binding<Bool>>, destination: <() -> _>, label: <() -> _>)

You can pass an empty closure to the destination parameter. It will never get called anyway. To do some action you put a button on top within a ZStack.

func navigationLinkStyle() -> some View {
    let never = Binding<Bool> { false } set: { _ in }
    return ZStack {
        NavigationLink(isActive: never, destination: { }) {
            Text("Item 1")  // your list cell view
        }
        Button {
           // do your action on tap gesture
        } label: {
            EmptyView()  // invisible placeholder
        }
    }
}
Laufwunder
  • 773
  • 10
  • 21
1

For accessibility you might need to mimic UIKit version of disclosure indicator. You don't need to implement it this way per se but if you use e.g. Appium for testing you might want to have it like this to keep tests succeeding

Apparently UIKit's disclosure indicator is a disabled button with some accessibility values so here's the solution:

struct DisclosureIndicator: View {
    var body: some View {
        Button {

        } label: {
            Image(systemName: "chevron.right")
                .font(.body)
                .foregroundColor(Color(UIColor.tertiaryLabel))
        }
        .disabled(true)
        .accessibilityLabel(Text("chevron"))
        .accessibilityIdentifier("chevron")
        .accessibilityHidden(true)
    }
}
ramzesenok
  • 5,469
  • 4
  • 30
  • 41
0

Or maybe create a fake one and use it, even if you tap you can call your events.

 NavigationLink(destination: EmptyView()) {
            HStack {
                Circle()
                 Text("TITLE")  
               
            }
        }
        .contentShape(Rectangle())
        .onTapGesture {
            print("ALERT MAYBE")
        }
zdravko zdravkin
  • 2,090
  • 19
  • 21
0

I created a custom NavigationLink that:

  1. Adds an action API (instead of having to push a View)
  2. Shows the disclosure indicator
  3. Ensures that List cell selection remains as-is

Usage

MYNavigationLink(action: {
  didSelectCell()
}) {
  MYCellView()
}

Code

import SwiftUI

struct MYNavigationLink<Label: View>: View {
  
  @Environment(\.colorScheme) var colorScheme
  
  private let action: () -> Void
  private let label: () -> Label
  
  init(action: @escaping () -> Void, @ViewBuilder label: @escaping () -> Label) {
    self.action = action
    self.label = label
  }
  
  var body: some View {
    Button(action: action) {
      HStack(spacing: 0) {
        label()
        Spacer()
        NavigationLink.empty
          .layoutPriority(-1) // prioritize `label`
      }
    }
    // Fix the `tint` color that `Button` adds
    .tint(colorScheme == .dark ? .white : .black) // TODO: Change this for your app
  }
}

// Inspiration:
// - https://stackoverflow.com/a/66891173/826435
private extension NavigationLink where Label == EmptyView, Destination == EmptyView {
   static var empty: NavigationLink {
       self.init(destination: EmptyView(), label: { EmptyView() })
   }
}
kgaidis
  • 14,259
  • 4
  • 79
  • 93
0

The easy solution is to add a view modifier which will add the arrow, than it will be easy to use on other screens:

struct DisclosureIndicatorModifier: ViewModifier {
    
    func body(content: Content) -> some View {
            
        HStack(spacing: 0) {
            content
            Spacer()
            Image(systemName: "chevron.right")
                                        .font(Font.system(size: 13, weight: .semibold, design: .default))
                                        .foregroundColor(Color(red: 0.771, green: 0.771, blue: 0.779))
        }
        .contentShape(Rectangle())
            
    }
}

extension View {
    func disclosureIndicator() -> some View {
        modifier(DisclosureIndicatorModifier())
    }
}

and to use just add the modifier like in the example bellow, where I have a List row with an icon and a title:

struct DashboardSiteRow: View {
    var title: String
    
    var body: some View {
        HStack {
            SquareIcon()
            
            Text(title)
           
        }
        .disclosureIndicator()
        
    }
}
rsergiu2003
  • 102
  • 3