SwiftUI 使用 INFO 按钮列出行

SwiftUI List rows with INFO button

UIKit 过去支持启用蓝色 info/disclosure 按钮的 TableView Cell。以下是在 SwiftUI 中生成的,但是让底层功能正常工作对 SwiftUI 初学者来说是一个挑战。

由以下代码生成:

struct Session: Identifiable {
    let date: Date
    let dir: String
    let instrument: String
    let description: String
    var id: Date { date }
}

final class SessionsData: ObservableObject {
    @Published var sessions: [Session]
        
    init() {
        sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
                    Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
                    Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
        ]
    }
    static func dateFromString(stringDate: String) -> Date {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
        return dateFormatter.date(from:stringDate)!
    }
}

struct SessionList: View {
    @EnvironmentObject private var sessionData: SessionsData
    
    var body: some View {
        NavigationView {
            List {
                ForEach(sessionData.sessions) { session in
                    SessionRow(session: session )
                }
            }
            .navigationTitle("Session data")
        }
        // without this style modification we get all sorts of UIKit warnings
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

struct SessionRow: View {
    var session: Session
    
    @State private var presentDescription = false
    
    var body: some View {
        HStack(alignment: .center){
            VStack(alignment: .leading) {
                Text(session.dir)
                    .font(.headline)
                    .truncationMode(.tail)
                    .frame(minWidth: 20)

                Text(session.instrument)
                    .font(.caption)
                    .opacity(0.625)
                    .truncationMode(.middle)
            }
            Spacer()
            // SessionGraph is a place holder for the Graph data.
            NavigationLink(destination: SessionGraph()) {
                // if this isn't an EmptyView then we get a disclosure indicator
                EmptyView()
            }
            // Note: without setting the NavigationLink hidden
            // width to 0 the List width is split 50/50 between the
            // SessionRow and the NavigationLink. Making the NavigationLink
            // width 0 means that SessionRow gets all the space. Howeveer
            // NavigationLink still works
            .hidden().frame(width: 0)

            Button(action: { presentDescription = true
                print("\(session.dir):\(presentDescription)")
            }) {
                Image(systemName: "info.circle")
            }
            .buttonStyle(BorderlessButtonStyle())

            NavigationLink(destination: SessionDescription(),
                           isActive: $presentDescription) {
                EmptyView()
            }
            .hidden().frame(width: 0)
        }
        .padding(.vertical, 4)
    }
}

struct SessionGraph: View {
    var body: some View {
        Text("SessionGraph")
    }
}

struct SessionDescription: View {
    var body: some View {
        Text("SessionDescription")
    }
}

问题出在 SessionGraph 的 NavigationLinks 行为中。选择作为行主体的 SessionGraph,传播到 SessionDescription!因此视图开始在不受控制的庄园中飞来飞去。

我已经看到了针对这个问题的几个明确的解决方案,但是 none 使用 XCode 12.3 & iOS 14.3

已经奏效了

有什么想法吗?

当您在列表行的背景中放置 NavigationLink 时,NavigationLink 仍然可以在点击时激活。即使有 .buttonStyle(BorderlessButtonStyle())(对我来说这看起来像个错误)。

一个可能的解决方案是将所有 NavigationLinks 移到 列表之外,然后从列表行内激活它们。为此,我们需要 @State 个保存激活状态的变量。然后,我们需要将它们作为 @Binding 传递给子视图,并在点击按钮时激活它们。

这是一个可能的例子:

struct SessionList: View {
    @EnvironmentObject private var sessionData: SessionsData
    
    // create state variables for activating NavigationLinks
    @State private var presentGraph: Session?
    @State private var presentDescription: Session?

    var body: some View {
        NavigationView {
            List {
                ForEach(sessionData.sessions) { session in
                    SessionRow(
                        session: session,
                        presentGraph: $presentGraph,
                        presentDescription: $presentDescription
                    )
                }
            }
            .navigationTitle("Session data")
            // put NavigationLinks outside the List
            .background(
                VStack {
                    presentGraphLink
                    presentDescriptionLink
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
    
    @ViewBuilder
    var presentGraphLink: some View {
        // custom binding to activate a NavigationLink - basically when `presentGraph` is set
        let binding = Binding<Bool>(
            get: { presentGraph != nil },
            set: { if ![=10=] { presentGraph = nil } }
        )
        // activate the `NavigationLink` when the `binding` is `true` 
        NavigationLink("", destination: SessionGraph(), isActive: binding)
    }
    
    @ViewBuilder
    var presentDescriptionLink: some View {
        let binding = Binding<Bool>(
            get: { presentDescription != nil },
            set: { if ![=10=] { presentDescription = nil } }
        )
        NavigationLink("", destination: SessionDescription(), isActive: binding)
    }
}
struct SessionRow: View {
    var session: Session

    // pass variables as `@Binding`...
    @Binding var presentGraph: Session?
    @Binding var presentDescription: Session?

    var body: some View {
        HStack {
            Button {
                presentGraph = session // ...and activate them manually
            } label: {
                VStack(alignment: .leading) {
                    Text(session.dir)
                        .font(.headline)
                        .truncationMode(.tail)
                        .frame(minWidth: 20)

                    Text(session.instrument)
                        .font(.caption)
                        .opacity(0.625)
                        .truncationMode(.middle)
                }
            }
            .buttonStyle(PlainButtonStyle())
            Spacer()
            Button {
                presentDescription = session
                print("\(session.dir):\(presentDescription)")
            } label: {
                Image(systemName: "info.circle")
            }
            .buttonStyle(PlainButtonStyle())
        }
        .padding(.vertical, 4)
    }
}