解除SwiftUI模式或细节视图的三种方式

336 阅读3分钟

在构建iOS和Mac应用程序时,想要以模态或通过将其推送到当前导航堆栈来呈现某些视图是非常常见的。例如,在这里,我们将一个MessageDetailView 作为模态呈现,使用SwiftUI内置的sheet 修改器与本地@State 属性相结合,以跟踪当前是否正在呈现详细视图:

struct MessageView: View {
    var message: Message
    @State private var isShowingDetails = false

    var body: some View {
        ScrollView {
            Text(message.body)
            ...
        }
        .navigationTitle(message.subject)
        .navigationBarItems(trailing: Button("Details") {
            isShowingDetails = true
        })
        .sheet(isPresented: $isShowingDetails) {
            MessageDetailsView(message: message)
        }
    }
}

但现在的问题是--一旦呈现了MessageDetailsView ,我们该如何解除它?一种方法是将我们上面的isShowingDetails 属性作为绑定注入我们的MessageDetailsView ,然后详细视图可以将其设置为false ,以解散自己:

struct MessageDetailsView: View {
    var message: Message
    @Binding var isPresented: Bool

    var body: some View {
        VStack {
            ...
            Button("Dismiss") {
                isPresented = false
            }
        }
    }
}

struct MessageView: View {
    var message: Message
    @State private var isShowingDetails = false

    var body: some View {
        ...
        .sheet(isPresented: $isShowingDetails) {
            MessageDetailsView(
                message: message,
                isPresented: $isShowingDetails
            )
        }
    }
}

虽然上述模式肯定是可行的,但它要求我们每次都要手动实现绑定连接,以使我们的用户能够解散一个模态。所以,一定有一个更方便的方法,对吗?

好消息是有的,那就是使用presentationMode 环境值,它让我们可以访问一个对象,这个对象可以用来解散任何视图,不管它是如何被呈现的:

struct MessageDetailsView: View {
    var message: Message
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        VStack {
            ...
            Button("Dismiss") {
                presentationMode.wrappedValue.dismiss()
            }
        }
    }
}

有了上述内容,我们就不必再手动注入我们的isShowingDetails 属性作为绑定--只要我们的工作表被驳回,SwiftUI就会自动将该属性设置为false 。作为额外的奖励,如果我们通过将我们的MessageDetailsView 推送到导航堆栈上,而不是作为工作表显示,上述模式甚至也能发挥作用。在这种情况下,当演示模式的dismiss 方法被调用时,我们的视图将从导航栈中 "弹出"。很好!

然而,上面的实现有一点很尴尬,那就是我们必须访问我们的环境值的wrappedValue ,以便能够调用其dismiss 方法(因为它实际上是一个Binding ,而不仅仅是一个原始值)。因此,为了解决这个问题,苹果在iOS 15(以及他们2021年的其他操作系统)中引入了上述API的一个新版本,它被简单地称为dismiss 。这个新的API给了我们一个可以直接调用的动作--像这样:

struct MessageDetailsView: View {
    var message: Message
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack {
            ...
            Button("Dismiss") {
                dismiss()
            }
        }
    }
}

现在,如果你已经读了一段时间Sundell写的Swift,那么你可能会认为我接下来要指出的是,上述dismiss 闭包可以直接传递给我们的Button ,作为其action ,但实际上不是这样的。事实证明,dismiss 不是一个闭包,而是一个结构,它使用了 Swift 相对较新的 调用为函数 的功能。

因此,如果我们想把dismiss 动作直接注入到我们的按钮中,那么我们就必须传递一个对其callAsFunction 方法的引用--像这样:

struct MessageDetailsView: View {
    var message: Message
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack {
            ...
            Button("Dismiss", action: dismiss.callAsFunction)
        }
    }
}

当然,将我们对dismiss 的调用包裹在一个闭包中并没有什么问题(事实上,在这种情况下我更喜欢这样做),但我只是认为我应该指出这一点,因为看到SwiftUI对上述API和其他类似的API采用调用为函数的方式是很有趣的。

这就是三种不同的方式来解散SwiftUI模式或细节视图--其中两种是向后兼容iOS 14和更早的版本,另一种是现代版本,最好是在针对iOS 15(或其兄弟操作系统)的应用中使用。

谢谢你的阅读!