SwiftUI 使 ForEach 列表行可以在编辑模式下正确点击以进行编辑

SwiftUI make ForEach List row properly clickable for edition in EditMode

描述

我有以下代码:

NavigationView {
    List {
        ForEach(someList) { someElement in
            NavigationLink(destination: someView()) {
                someRowDisplayView()
            } //: NavigationLink
        } //: ForEach
    } //: List
} //: NavigationView

基本上,它会显示 class 个先前由用户输入填充的对象的动态列表。我有一个“addToList()”函数,它允许用户添加一些元素(比如姓名、电子邮件等),一旦用户确认,它就会将该元素添加到此列表中。然后我有一个视图,它通过 ForEach 显示列表,然后按照上面的代码示例对每个元素进行 NavigationLink

单击此列表中的某个元素后,用户应正确重定向到目标视图,如 NavigationLink 所示。这一切都很好。

我想要的

这是我遇到的问题:我希望用户能够编辑此列表中某行的内容,而不必删除该行并重新添加另一行一.

我决定使用 SwiftUI 的 EditMode() 功能。这就是我想出的:

@State private var editMode = EditMode.inactive
[...]
NavigationView {
    List {
        ForEach(someList) { someElement in
            NavigationLink(destination: someView()) {
                someRowDisplayView()
            } //: NavigationLink
        } //: ForEach
    } //: List
    .toolbar {
        ToolbarItem(placement: .navigationBarLeading) {
            EditButton()
        }
    }
    .environment(\.editMode, $editMode)
} //: NavigationView

当我点击 Edit 按钮时,EditMode 被正确触发。但我注意到列表行在编辑模式下不可点击,这很好,因为我不希望它在编辑模式下遵循 NavigationLink

虽然 我想要的是将用户重定向到允许编辑点击行的视图,或者更好的是,向用户呈现一个版本 sheet.

我试过的

因为我无法在编辑模式下点击该行,所以我尝试了几种技巧,但其中 none 的结果如我所愿。这是我的尝试。

.simultaneousGesture

我试图添加一个 .simultaneousGesture 修改到我的 NavigationLink 并切换 @State 变量以显示版本 sheet.

@State private var isShowingEdit: Bool = false
[...]
.simultaneousGesture(TapGesture().onEnded{
    isShowingEdit = true
})
.sheet(isPresented: $isShowingEdit) {
    EditionView()
}

这根本行不通。似乎这个 .simultaneousGesture 以某种方式破坏了 NavigationLink,点击成功就像五分之一。

它不起作用 即使在编辑模式上添加条件,如:

if (editMode == .active) {
    isShowingEdit = true
}

在非版本模式下,NavigationLink 仍然有问题。但我注意到,一旦进入编辑模式,它就可以满足我的要求。

视图上的修饰符条件扩展

在上次失败后,我的第一个想法是我需要触发点击手势 仅在编辑模式下,这样在非编辑模式下视图甚至不会知道它有一个点击手势。

我决定添加 View 结构的扩展,以便为修饰符定义条件(我在 Whosebug 的某个地方找到了这个代码示例):

extension View {
    @ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

然后在我的主要代码中,我将其添加到 NavigationLink:

而不是之前尝试的 .simultaneousGesture 修饰符
.if(editMode == .active) { view in
    view.onTapGesture {
        isShowingEdit = true
    }
}

它奏效了,当用户在编辑模式和普通非版本模式下点击一行时,我能够显示这个版本视图仍然有效,因为它正确触发了 NavigationLink。

剩余问题

但有些事情让我很困扰,感觉这不像是一种自然或本能的行为。点击后,该行没有显示任何反馈,就像它在遵循 NavigationLink 时在非版本模式下显示的那样:它甚至没有突出显示很短的时间。
点击手势修改器只是执行我要求的代码 ,没有动画、反馈或任何东西

  • 我希望用户知道并看到点击了哪一行并且我希望它突出显示就像点击 NavigationLink 时一样:只是让它看起来像该行实际上被“点击”。我希望它有意义。

  • 我还希望当用户点击 整行 而不仅仅是文本可见的部分时触发代码。目前,点击行的空白字段 不会执行任何操作。它必须有文字。

更好的解决方案是阻止我将此类条件修饰符和扩展应用于视图结构,因为如果可能的话,我当然更喜欢更自然、更好的方法。

我是 Swift 的新手,所以也许有更简单或更好的解决方案,我愿意遵循最佳做法。

在这种情况下我怎样才能完成我想要的?
感谢您的帮助。

附加信息

我目前正在使用列表中的 .onDelete.onMove 实现以及 SwiftUI 版本模式,我想继续使用它们。
我正在开发最小 iOS 14 的应用程序,使用 Swift 语言和 SwiftUI 框架。
如果您需要更多代码示例或更好的解释,请随时提出,我将编辑我的问题。

最小工作示例

Yrb 提问

import SwiftUI
import PlaygroundSupport

extension View {
    @ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

struct SomeList: Identifiable {
    let id: UUID = UUID()
    var name: String
}

let someList: [SomeList] = [
    SomeList(name: "row1"),
    SomeList(name: "row2"),
    SomeList(name: "row3")
]

struct ContentView: View {
    @State private var editMode = EditMode.inactive
    @State private var isShowingEditionSheet: Bool = false
    
    var body: some View {
        NavigationView {
            List {
                ForEach(someList) { someElement in
                    NavigationLink(destination: EmptyView()) {
                        Text(someElement.name)
                    } //: NavigationLink
                    .if(editMode == .active) { view in
                        view.onTapGesture {
                            isShowingEditionSheet = true
                        }
                    }
                } //: ForEach
                .onDelete { (indexSet) in }
                .onMove { (source, destination) in }
            } //: List
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    EditButton()
                }
            }
            .environment(\.editMode, $editMode)
            .sheet(isPresented: $isShowingEditionSheet) {
                Text("Edition sheet")
            }
        } //: NavigationView
    }
}

PlaygroundPage.current.setLiveView(ContentView())

正如我们在评论中所讨论的那样,您的代码会执行列表中的所有操作,除了在点击披露 V 形标志时突出显示该行。要更改一行的背景颜色,请使用 .listRowBackground()。要仅突出显示一行,您需要使 ForEach 数组中的 id 行可识别。然后你有一个可选的 @State 变量来保存行 ID 的值,并在你的 .onTapGesture 中设置行 ID。最后,您使用 DispatchQueue.main.asyncAfter() 在一定时间后将变量重置为 nil。这给了你一个瞬间的亮点。

但是,一旦您走这条路,您还必须管理 NavigationLink 的背景突出显示。这带来了其自身的复杂性。您需要使用 NavigationLink(destination:,isActive:,label:) 初始值设定项,使用 setter 和 getter 创建绑定,并在 getter、运行 中添加高亮代码。

struct ContentView: View {
    @State private var editMode = EditMode.inactive
    @State private var isShowingEditionSheet: Bool = false
    @State private var isTapped = false
    @State private var highlight: UUID?

    var body: some View {
        NavigationView {
            List {
                ForEach(someList) { someElement in
                    // You use the NavigationLink(destination:,isActive:,label:) initializer
                    // Then create your own binding for it
                    NavigationLink(destination: EmptyView(), isActive: Binding<Bool>(
                        get: { isTapped },
                        // in the set, you can run other code
                        set: {
                            isTapped = [=10=]
                            // Set highlight to the row you just tapped
                            highlight = someElement.id
                            // Reset the row id to nil
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                highlight = nil
                            }
                        }
                    )) {
                        Text(someElement.name)
                    } //: NavigationLink
                    // identify your row
                    .id(someElement.id)
                    .if(editMode == .active) { view in
                        view.onTapGesture {
                            // Set highlight to the row you just tapped
                            highlight = someElement.id
                            // Reset the row id to nil
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                highlight = nil
                            }
                            isShowingEditionSheet = true
                        }
                    }
                    .listRowBackground(highlight == someElement.id ? Color(UIColor.systemGray5) : Color(UIColor.systemBackground))
                } //: ForEach
                .onDelete { (indexSet) in }
                .onMove { (source, destination) in }
            } //: List

            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    EditButton()
                }
            }
            .environment(\.editMode, $editMode)
            .sheet(isPresented: $isShowingEditionSheet) {
                Text("Edition sheet")
            }
        } //: NavigationView
    }
}