使用闭包进行SwiftUI视图动作传递的方法(附代码)

359 阅读2分钟

在使用交互式SwiftUI视图时,我们经常使用闭包来定义我们希望在各种事件发生时执行的动作。例如,下面的AddItemView 有两个交互式元素,一个TextField 和一个Button ,它们都能让用户在我们的应用程序中添加一个新的基于文本的Item

struct AddItemView: View {
    var handler: (Item) -> Void
    @State private var title = ""

    var body: some View {
        HStack {
            TextField("Add item",
                text: $title,
                onCommit: {
                    guard !title.isEmpty else {
                        return
                    }
                    
                    let item = Item(title: title)
                    handler(item)
                    title = ""
                }
            )
            Button("Add") {
                let item = Item(title: title)
                handler(item)
                title = ""
            }
            .disabled(title.isEmpty)
        }
    }
}

除了在我们的文本字段的onCommit 动作中领先的guard 语句(在我们的按钮动作中不需要,因为我们在文本为空时禁用按钮),我们的两个闭包是完全相同的,所以通过将这些动作从我们的视图的body 中移开,摆脱代码重复的来源将是非常好的。

要做到这一点的一个方法是使用一个计算属性来创建我们的闭包。这可以让我们一次性定义我们的逻辑,如果我们还包括我们的TextField 所需要的guard 语句,那么我们就可以为我们的两个UI控件使用完全相同的闭包实现:

private extension AddItemView {
    var addAction: () -> Void {
        return {
            guard !title.isEmpty else {
                return
            }

            let item = Item(title: title)
            handler(item)
            title = ""
        }
    }
}

有了上面的方法,我们现在可以简单地将我们新的addAction 属性传递给我们的两个子视图,我们已经成功地摆脱了代码的重复,而且我们的视图的body 实现现在也更加紧凑。

struct AddItemView: View {
    var handler: (Item) -> Void
    @State private var title = ""

    var body: some View {
        HStack {
            TextField("Add item",
                text: $title,
                onCommit: addAction
            )
            Button("Add", action: addAction)
                .disabled(title.isEmpty)
        }
    }
}

虽然以上是一个完美的解决方案,但在SwiftUI的上下文中,还有一个最初可能并不明显的选择,那就是使用与使用UIKit的目标/动作模式相同的技术--将我们的动作处理程序定义为一个方法,而不是一个闭包。

要做到这一点,首先让我们把之前的addAction 属性重构为一个addItem 方法,看起来像这样:

private extension AddItemView {
    func addItem() {
        guard !title.isEmpty else {
            return
        }

        let item = Item(title: title)
        handler(item)
        title = ""
    }
}

然后,就像我们之前将我们的addAction 属性传递给我们的TextViewButton 一样,我们现在可以对我们的addItem 方法做同样的事情--这给了我们下面的实现:

struct AddItemView: View {
    var handler: (Item) -> Void
    @State private var title = ""

    var body: some View {
        HStack {
            TextField("Add item",
                text: $title,
                onCommit: addItem
            )
            Button("Add", action: addItem)
                .disabled(title.isEmpty)
        }
    }
}

在使用SwiftUI时,我们很容易陷入这样的误区,即认为某个视图的布局、子视图和动作都需要在其body ,如果我们仔细想想,这正是使用UIKit时经常导致大量视图控制器的那种方法。

然而,由于SwiftUI的高度可组合设计,通常很容易将一个视图的body 分割成独立的部分,这甚至可能不需要创建任何新的View 类型。有时,我们所要做的就是把我们的一些逻辑提取到一个单独的方法中,最后我们会得到更优雅的代码,更容易阅读和维护。