'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:

Current Behaviour

Wanted Behaviour:

Wanted Behaviour



Solution 1:[1]

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

Solution 2:[2]

The possible approach is to make row with custom chevron, like in demo below (tested with Xcode 12.1 / iOS 14.1)

demo

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

Solution 3:[3]

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.

Screenshot

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 George
Solution 2 Asperi
Solution 3 iComputerfreak