简体   繁体   中英

SwiftUI List disclosure indicator without NavigationLink

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 .

披露按钮图像

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)
                    }
                }

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.

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.

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")
        }

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
        }
    }
}

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 eg 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)
    }
}

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.

突出显示一个单元格的 SwiftUI 列表的屏幕截图

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() })
   }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM