SwiftUI基础篇Sheet

4,308 阅读4分钟

Sheet

概述

文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,Sheet展示部分调试结果,不过测试代码是齐全的。如果想要运行结果,可以移步Github下载code -> github案例链接

1、使用Sheet-Present一个新视图

SwiftUI的Sheets用于视图上present新视图,同时仍然允许用户在准备好时向下拖动以关闭新视图。要使用sheet,给他一些东西来显示(文本、图片、自定义View等),添加一个Bool值来定义DetailView是否应该被显示,然后将其作为模态sheet附加到主视图上。

//例如,创建一个简单的DetailView,然后在点击Button时从ContentView-present它
struct SheetView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        Button("Press to dismiss") {
            dismiss()
        }
        .font(.title)
        .padding()
        .background(.black)
    }
}

struct FFPresentSheets: View {
    @State private var showingSheet = false
        
    var body: some View {
        Button("Show Sheet") {
            showingSheet.toggle()
        }
        .sheet(isPresented: $showingSheet) {
            SheetView()
        }
    }
}

如果在iOS14以下,使用@environment(.presentationMode) var presentationMode 和presentationMode.wrappedValue.dismiss()来代替。与导航push不同,Sheet不需要NavigationStack。 在iOS上,如果想关闭向下拖动关闭的操作,使用fullScreenCover()修饰符。

2、使用Sheets多重present

如果想在SwiftUI中显示多个View页面,通过从第一个View-present第二个视图来实现,不可以将两个sheet()修饰符同时附加到一个父视图上。

struct FFPresentSheetsMultiple: View {
    @State private var showingFirst = false
    @State private var showingSecond = false
    
    var body: some View {
        VStack {
            Button("Show First Sheet") {
                showingFirst = true
            }
        }
        .sheet(isPresented: $showingFirst) {
            Button("Show Second Sheet") {
                showingSecond = true
            }
            .sheet(isPresented: $showingSecond) {
                Text("Second Sheet")
            }
        }
    }
}

使用这种方法,两个Sheet都将正确显示。如果把两个Sheet()修饰符放在同一个父元素中,SwiftUI会失效,只显示第一个sheet。

调试结果

Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 16.57.22.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 16.57.25.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 16.57.29.png

3、Sheet的视图如何dismiss

当使用Sheet显示了一个SwiftUI视图时,通常想要在某些事情完成后关闭那个视图。例如当用户点击一个按钮时。在SwiftUI中有两种解决这个问题的方法。

3.1、@Environment(.dismiss)

第一个方法是告诉视图使用它自己的Presentation mode的环境变量来关闭。任何视图都可以关闭自己,不管它是如何present的。使用@Environment(\.dismiss)

struct DismissingView_015_01: View {
    @Environment(\.dismiss) var dismiss
    var body: some View {
        Button("Dismiss me") {
            dismiss()
        }
    }
}

struct FFSheetViewDismiss: View {
    @State private var showingDetail = false
    @State private var showingDetail2 = false
    
    var body: some View {
        Button("Show Detail") {
            showingDetail = true
        }
        .sheet(isPresented: $showingDetail) {
            DismissingView_015_01()
        }
    }
}

3.2、@Binding state

另外一种选择是将绑定传递到所显示的Detail视图中,这样就可以将绑定值更改回false。仍然需要在sheet视图中拥有某种state属性,但现在它作为绑定传递给Detail视图。使用这种方法,视图将其绑定设置为false也会更新原始视图中的状态,导致Detail视图Dismiss

struct DismissingView_015_02: View {
    @Binding var isPresented: Bool
    
    var body: some View {
        Button("Dismiss me") {
            isPresented = false
        }
    }
}

struct FFSheetViewDismiss: View {
    @State private var showingDetail = false
    @State private var showingDetail2 = false
    
    var body: some View {
        Button("Show Detail2") {
            showingDetail2 = true
        }
        .sheet(isPresented: $showingDetail2) {
            DismissingView_015_02(isPresented: $showingDetail2)
        }
    }
}

3、fullScreenCover-present全屏视图

当想要全屏的时候,SwiftUI的fullScreenCover()修饰符提供了一样显示方式,在代码中,他的工作原理几乎与Sheet一样。常规的sheet可以通过向下拖动来关闭,但是使用fullScreenCover-present的视图是不能的,因此,提供一种方法来dismiss新视图是要解决的问题。fullScreenCover()在macOS上不可用。

struct FullScreenModalView_015: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        ZStack {
            Color.primary.ignoresSafeArea(edges: .all)
            Button("Dismiss modal") {
                dismiss()
            }
        }
    }
}
struct FFSheetFullScreenCover: View {
    @State private var isPresented = false
    
    var body: some View {
        Button("Present!") {
            isPresented.toggle()
        }
        .fullScreenCover(isPresented: $isPresented, content: FullScreenModalView_015.init)
    }
}

4、显示Popover视图

SwiftUI有一个专门的修饰器来显示弹出窗口,在iPad上显示为漂浮的气泡,在iOS上是滑动。要显示弹出窗口,需要一些状态来确定弹出窗口是否可见。与alert不同,弹出窗口可以包含任何类型的视图,所以,只要把需要的东西放在弹出窗口中就可以。

struct FFSheetPopover: View {
    @State private var showingPopover = false
    
    var body: some View {
        Button("Show Menu") {
            showingPopover = true
        }
        .popover(isPresented: $showingPopover) {
            Text("Your content here")
                .font(.headline)
                .padding()
        }
    }
}

5、防止Sheet被dismiss

SwiftUI提供了interactiveDismissDisabled()修饰符来控制用户是否可以向下滑动来关闭一个Sheet View。例如,如果用户必须接受的协议,只用同意才能关闭。

5.1、interactiveDismissDisabled()

最简单的方法是,interactiveDismissDisabled()修饰符附加到你的Sheet Content上。

struct ExampleSheet_015_01: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
            Text("Sheet View")
            Button("Dismiss", action: close)
        }
        .interactiveDismissDisabled()
    }
    
    func close() {
        dismiss()
    }
}

struct FFSheetSwipe: View {
    @State private var showingSheet = false
    @State private var showingSheet1 = false
    
    var body: some View {
        Button("Show Sheet") {
            showingSheet.toggle()
        }
        .sheet(isPresented: $showingSheet, content: ExampleSheet_015_01.init)
    }
}

5.2、设定dismiss的条件

可以将一个bool值绑定到修饰符,以允许swipe仅在成功满足某些条件时才取消。

struct ExampleSheet_015_02: View {
    @State private var termsAccepted = false
    
    var body: some View {
        VStack {
            Text("Terms and conditions")
                .font(.title)
            Text("Lots of legalese here")
            Toggle("Accept", isOn: $termsAccepted)
        }
        .padding()
        .interactiveDismissDisabled(!termsAccepted)
    }
}

struct FFSheetSwipe: View {
    @State private var showingSheet1 = false
    
    var body: some View {
        Button("Show Sheet 1") {
            showingSheet1.toggle()
        }
        .sheet(isPresented: $showingSheet1, content: ExampleSheet_015_02.init)
    }
}

6、显示一个bottom-sheet

SwiftUI的presentationsDetents()修饰符可以创建从视图底部向上滑动的Sheet,可以不占满全屏,至于多少,根据需求来控制。

6.1、presentationDetents([.medium, .large])

同时支持.medium和.large,SwiftUI将创建一个调整大小的句柄,让用户在这两个size之间调整表单

struct FFSheetShowBottom: View {
    @State private var showingCredits = false
    
    var body: some View {
        Button("Show Credits") {
            showingCredits.toggle()
        }
        .sheet(isPresented: $showingCredits) {
            Text("同时支持.medium和.large")
                .presentationDetents([.medium, .large])
        }
    }
}

6.2、presentationDragIndicator(.hidden)

隐藏提示条

struct FFSheetShowBottom: View {
    @State private var showingCredits2 = false
    
    var body: some View {

        Button("Show Credits 2") {
            showingCredits2.toggle()
        }
        .sheet(isPresented: $showingCredits2) {
            Text("隐藏提示条")
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.hidden)
        }
    }
}

6.3、presentationDragIndicator(.hidden)

即使有自定义的显示控件,当有一个高度比较小的class时,sheet也会自动占据整个屏幕。比如,横向的iPhone。如果想支持此场景,请确保提供一种方法来关闭sheet view。即使指定一个内置大小外,还可以提供一个范围为0-1的自定义分数。例如,创建一个占用屏幕底部15%的sheet View

struct FFSheetShowBottom: View {
    @State private var showingCredits3 = false
    
    var body: some View {
        Button("Show Credits 3") {
            showingCredits3.toggle()
        }
        .sheet(isPresented: $showingCredits3) {
            Text("创建百分比的占用高度")
                .presentationDetents([.fraction(0.15)])
        }
    }
}

6.4、presentationDetents([.height(300)])

创建一个精确的高度

struct FFSheetShowBottom: View {
    @State private var showingCredits4 = false
    
    var body: some View {
        Button("Show Credits 4") {
            showingCredits4.toggle()
        }
        .sheet(isPresented: $showingCredits4) {
            Text("固定高度")
                .presentationDetents([.height(300)])
        }
    }
}

6.5、动态切换高度

可以根据需要给视图附加任意多的detens,只要把他们全部添加到detens的集合中,SwiftUI会自动处理。例如,可以让用户在从10%切换到100%

struct FFSheetShowBottom: View {
    @State private var showingCredits5 = false
    let heights = stride(from: 0.1, to: 1.0, by: 0.1).map { PresentationDetent.fraction($0) }
    
    var body: some View {
        Button("Show Credits 5") {
            showingCredits5.toggle()
        }
        .sheet(isPresented: $showingCredits5) {
            Text("动态切换高度")
                .presentationDetents(Set(heights))
        }
    }
}

调试结果

Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 17.58.09.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 18.01.27.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 18.01.30.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 18.01.33.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 18.01.37.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 18.01.41.png

7、如何展示白画面的占位图

SwiftUI有一个专用的ContentUnavailableView视图,在没有数据的时候向用户展示非空画面。例如,在用户执行了搜索操作之后,并未搜索到内容,使用此View。

7.1、基础样式

默认提供一个放大镜图标,由标题和副标题构成,用来展示用户并未搜索到具体内容

struct FFContentUnavailable: View {
    var body: some View {
        ContentUnavailableView.search
    }
}

7.2、自定义文本

如果有需求,可以对其进行自定义文本,以添加用户搜索的内容

struct FFContentUnavailableCustom: View {
    var body: some View {
        ContentUnavailableView.search(text: "Life, the Universe, and Everything")
    }
}

7.3、完全自定义

完全自定义所有的内容

struct FFContentUnavailableCustomAll: View {
    var body: some View {
        ContentUnavailableView("No favorites", systemImage: "star", description: Text("You don't have any favorites yet."))
            .symbolVariant(.slash)
    }
}

调试结果

Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 19.22.35.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 19.23.00.png Simulator Screenshot - iPhone 14 Pro - 2023-08-18 at 19.23.39.png