SwiftUI 叠加 Sheet 终极方案:解决多模态视图难题
2025-01-28 05:39:15
SwiftUI 叠加 Sheet 的解决方案
问题
在 SwiftUI 应用开发中,sheet
修饰符用于呈现模态视图,用户交互主要围绕此。 但当应用需要自动、非用户触发式地弹出 sheet 时,特别是存在多个潜在的自动 sheet,sheet
修饰符的局限性就显现出来。标准的 sheet
修饰符,无法简单叠加展示多个 sheet;一旦存在父 sheet,嵌套的子 sheet
将不会如预期展现,这直接导致了嵌套 sheet
的困境,开发者需要额外方式管理。比如下方代码,sheet2Open
所绑定的sheet 是不可能展示的,因为父view已经被 sheet1Open
所绑定 sheet 给覆盖了。
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.onTapGesture {
sheet1Open.toggle()
}
.sheet(isPresented: $sheet1Open) {
Sheet1Veiw()
.onTapGesture {
sheet2Open.toggle()
}
}
.sheet(isPresented: $sheet2Open) {
Sheet2Veiw()
}
常见原因
SwiftUI 的 sheet
修饰符默认情况下只能一次呈现一个视图。当一个 sheet
已经呈现,再次尝试呈现另一个 sheet,由于视图层级关系以及 Swift UI sheet present 的机制导致后面的 sheet 将无法按预期展示。直接嵌套 .sheet
可能会造成某些 sheet 无法被呈现,而且会导致整个 app 的结构变得难以维护和理解。
解决方案一:环境对象(Environment Object)
使用一个集中管理 sheet
展示状态的 environment object,它保存待展示 sheet
的数据以及显示控制。 当 environment object 的状态改变,绑定它的 sheet 就可以动态地展现或隐藏,从而模拟多个叠加的 sheet 展示,实现自动 sheet 的管理。
-
定义
SheetManager
类:该类实现ObservableObject
协议,以便 SwiftUI 在其更改时更新视图。 包含一个枚举类型用于定义sheet类型,以及@Published
属性,当sheet 类型更改时通知观察者并展示响应视图。import SwiftUI enum SheetType: Identifiable { case sheet1 case sheet2 case none var id: Self {self} } class SheetManager: ObservableObject { @Published var currentSheet: SheetType = .none func showSheet(_ sheet: SheetType) { self.currentSheet = sheet } func hideSheet(){ self.currentSheet = .none } }
-
在最外层父级视图上初始化这个
SheetManager
,并且把它插入环境中,方便所有子View 可以通过EnvironmentObject
的方法来读取它。
@main
struct YourApp: App {
@StateObject var sheetManager = SheetManager()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(sheetManager)
}
}
}
- 在你需要呈现 sheet 的视图中,读取
SheetManager
,并通过其发布的currentSheet
属性绑定.sheet
,使用 switch语句来管理sheet的类型以及展示的内容:
struct ContentView: View {
@EnvironmentObject var sheetManager: SheetManager
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.onAppear {
// for example, pop first sheet
DispatchQueue.main.asyncAfter(deadline: .now()+2){
sheetManager.showSheet(.sheet1)
}
}
.sheet(item: $sheetManager.currentSheet) { currentSheet in
switch currentSheet {
case .sheet1 :
Sheet1Veiw()
case .sheet2:
Sheet2Veiw()
case .none:
EmptyView()
}
}
}
}
struct Sheet1Veiw:View {
@EnvironmentObject var sheetManager: SheetManager
var body: some View{
Text("Sheet1 view")
.onTapGesture {
sheetManager.showSheet(.sheet2)
}
}
}
struct Sheet2Veiw:View {
var body: some View{
Text("Sheet2 view")
}
}
优势 : 管理集中,更容易跟踪和调整 sheet 的行为。适用于复杂的多个 sheet 展示场景,可复用性高, 代码更加整洁易读,逻辑结构清晰。
安全提示 : Environment Object 的实例应该谨慎管理,避免过早释放或重新初始化,不然可能会造成数据异常或UI的渲染错误。建议使用 @StateObject
修饰根视图所引用的环境对象,并且注意及时地取消不再需要的订阅或更改监听。
解决方案二:自定义 Modal View
在SwiftUI中,可以通过ZStack 和一个 overlay view 来自定义模态视图的效果。通过在 ZStack 最上面叠加 View 来模仿 Sheet 的呈现方式。这个方法提供了更大的灵活性,允许你在视图层级之上完全控制 sheet 的行为,如出现、消失的动画,并且可以直接控制 sheet 的展示,不再受 .sheet
修饰符的限制。
-
创建一个新的
ModalView
,该 view 将会充当一个自定义的sheet
。 它会接收一些绑定数据 (例如$isPresent
) 用于显示/隐藏,同时接收具体展示的内容作为@ViewBuilder
closure。import SwiftUI struct ModalView<Content: View>: View { @Binding var isPresented: Bool var content: () -> Content var body: some View { ZStack { if isPresented { Color.black.opacity(0.5).edgesIgnoringSafeArea(.all) .transition(.opacity) content() .transition(.move(edge: .bottom)) } } .animation(.spring(), value: isPresented) } }
-
在父级 View 中管理需要显示的 Model ,使用自定义
ModalView
展示不同内容的模态框。
struct ContentView: View {
@State private var isSheet1Present = false
@State private var isSheet2Present = false
var body: some View {
ZStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
.onTapGesture{
isSheet1Present.toggle()
}
}
ModalView(isPresented: $isSheet1Present) {
Sheet1Veiw2(isPresented:$isSheet1Present,isSheet2Present:$isSheet2Present )
}
ModalView(isPresented: $isSheet2Present){
Sheet2Veiw2(isSheet2Present:$isSheet2Present)
}
}
.onAppear {
// For example, auto present sheet after 2s.
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
isSheet1Present.toggle()
}
}
}
}
struct Sheet1Veiw2:View {
@Binding var isPresented : Bool
@Binding var isSheet2Present :Bool
var body: some View{
Text("Sheet1 view")
.onTapGesture {
isPresented = false
isSheet2Present = true
}
}
}
struct Sheet2Veiw2:View {
@Binding var isSheet2Present :Bool
var body: some View{
Text("Sheet2 view")
.onTapGesture {
isSheet2Present = false
}
}
}
优势 : 提供对 Sheet 展现和行为完全的掌控,可以定制更复杂的动画或转场。可以自由地嵌套 sheet,不受原生 .sheet
的限制。
安全提示 : 记得为自定义 modal 加入合适的透明遮罩背景,以及提供方便的取消关闭方式,以保证最佳的用户体验。 在自定义 transition 的过程中,要注意性能以及流畅性,使用缓存视图,可以避免过渡过程的视图重复加载问题。
通过上述方案,开发者可以解决 SwiftUI 中叠加 Sheet 的难题。 选择何种方案,应该取决于应用复杂度,对定制的需求程度,以及性能考量。 使用合适的技巧,有助于建立健壮且高效的应用。