我的ContentView带有两个不同的模式视图,因此我对两者都使用sheet(isPresented:)
,但似乎只显示了最后一个。我该如何解决这个问题?还是真的不可能在SwiftUI的视图上使用多个工作表?
struct ContentView: View {
@State private var firstIsPresented = false
@State private var secondIsPresented = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
.sheet(isPresented: $secondIsPresented) {
Text("Only the second modal view works!")
}
}
}
}
上面的代码编译时没有警告(Xcode 11.2.1)。
答案 0 :(得分:42)
最佳方法,它也适用于 iOS 14 :
enum ActiveSheet: Identifiable {
case first, second
var id: Int {
hashValue
}
}
struct YourView: View {
@State var activeSheet: ActiveSheet?
var body: some View {
VStack {
Button(action: {
activeSheet = .first
}) {
Text("Activate first sheet")
}
Button(action: {
activeSheet = .second
}) {
Text("Activate second sheet")
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .first:
FirstView()
case .second:
SecondView()
}
}
}
}
在此处了解更多信息:https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)
要隐藏工作表,只需设置activeSheet = nil
答案 1 :(得分:29)
还可以将工作表添加到放置在视图背景中的EmptyView。这可以多次执行:
.background(EmptyView()
.sheet(isPresented: isPresented, content: content))
答案 2 :(得分:7)
只需将按钮和.sheet调用组合在一起即可完成此操作。如果您有一个领先者和一个落后者,那就这么简单。但是,如果在开头或结尾有多个导航项,则需要将它们包装在HStack中,并将每个按钮及其工作表调用包装在VStack中。
这是两个尾随按钮的示例:
trailing:
HStack {
VStack {
Button(
action: {
self.showOne.toggle()
}
) {
Image(systemName: "camera")
}
.sheet(isPresented: self.$showOne) {
OneView().environment(\.managedObjectContext, self.managedObjectContext)
}
}//showOne vstack
VStack {
Button(
action: {
self.showTwo.toggle()
}
) {
Image(systemName: "film")
}
.sheet(isPresented: self.$showTwo) {
TwoView().environment(\.managedObjectContext, self.managedObjectContext)
}
}//show two vstack
}//nav bar button hstack
答案 3 :(得分:7)
在其中创建自定义按钮视图和调用表可以解决此问题。
struct SheetButton<Content>: View where Content : View {
var text: String
var content: Content
@State var isPresented = false
init(_ text: String, @ViewBuilder content: () -> Content) {
self.text = text
self.content = content()
}
var body: some View {
Button(text) {
self.isPresented.toggle()
}
.sheet(isPresented: $isPresented) {
self.content
}
}
}
ContentView会更干净。
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 20) {
SheetButton("First modal view") {
Text("First modal view")
}
SheetButton ("Second modal view") {
Text("Only the second modal view works!")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
当打开工作表取决于列表行的内容时,此方法也可以正常工作。
struct ContentView: View {
var body: some View {
NavigationView {
List(1...10, id: \.self) { row in
SheetButton("\(row) Row") {
Text("\(row) modal view")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
答案 4 :(得分:5)
从 iOS 和 iPadOS 14.5 Beta 3 开始,无论何时公开发布,多张工作表都会按预期工作,并且不需要其他答案中的任何解决方法。来自发行说明:
<块引用>您现在可以应用多个 sheet(isPresented:onDismiss:content:)
和
同一视图中的 fullScreenCover(item:onDismiss:content:)
修饰符
等级制度。 (74246633)
答案 5 :(得分:4)
除了Rohit Makwana's answer之外,我还找到了一种将工作表内容提取到函数中的方法,因为编译器很难检查我巨大的View
的类型。
extension YourView {
enum Sheet {
case a, b
}
@ViewBuilder func sheetContent() -> some View {
if activeSheet == .a {
A()
} else if activeSheet == .b {
B()
}
}
}
您可以通过以下方式使用它:
.sheet(isPresented: $isSheetPresented, content: sheetContent)
它使代码更整洁,也减轻了编译器的压力。
答案 6 :(得分:4)
请尝试以下代码
enum ActiveSheet {
case first, second
}
struct ContentView: View {
@State private var showSheet = false
@State private var activeSheet: ActiveSheet = .first
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.showSheet = true
self.activeSheet = .first
}
Button ("Second modal view") {
self.showSheet = true
self.activeSheet = .second
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(isPresented: $showSheet) {
if self.activeSheet == .first {
Text("First modal view")
}
else {
Text("Only the second modal view works!")
}
}
}
}
}
答案 7 :(得分:2)
这对于我的应用程序非常有效,在iOS 13.x上可以进行三种工作表演示。有趣的行为始于iOS14。由于应用程序启动的某些原因,当我选择要显示的工作表时,状态变量未设置,工作表显示为空白屏幕。如果我继续选择第一个选项,它将继续显示空白页。一旦我选择了第二个选择(不同于第一个选择),就设置了变量并显示了正确的工作表。首先选择哪张纸都没有关系,同样的行为也会发生。
错误?还是我错过了一些东西。除了3个工作表选项外,我的代码几乎与上面的代码相同,并且我有一个自定义按钮,该按钮带有一个参数()-> Void,可在按下按钮时运行。在iOS 13.x上运行良好,但在iOS 14中效果不佳。
戴夫
答案 8 :(得分:2)
此解决方案适用于 iOS 14.0
此解决方案使用 .sheet(item:, content:) 构造
struct ContentView: View {
enum ActiveSheet: Identifiable {
case First, Second
var id: ActiveSheet { self }
}
@State private var activeSheet: ActiveSheet?
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
activeSheet = .First
}
Button ("Second modal view") {
activeSheet = .Second
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .First:
Text("First modal view")
case .Second:
Text("Only the second modal view works!")
}
}
}
}
}
答案 9 :(得分:1)
另一种在一个视图中显示许多工作表的简单方法:
每个视图私有变量都有其自己的Bool @State值和.sheet(isPresented:...调用
易于实现,所有必要操作都集中在一个地方。 在iOS 13,iOS 14,预览中可以确定
import SwiftUI
struct OtherContentView: View {
var body: some View {
Form {
Section {
button1
}
Section {
button2
}
Section {
button3
}
Section {
button4
}
}
}
@State private var showSheet1 = false
private var button1: some View {
Text("Sheet 1")
.onTapGesture { showSheet1 = true }
.sheet(isPresented: $showSheet1) { Text("Modal Sheet 1") }
}
@State private var showSheet2 = false
private var button2: some View {
Text("Sheet 2")
.onTapGesture { showSheet2 = true }
.sheet(isPresented: $showSheet2) { Text("Modal Sheet 2") }
}
@State private var showSheet3 = false
private var button3: some View {
Text("Sheet 3")
.onTapGesture { showSheet3 = true }
.sheet(isPresented: $showSheet3) { Text("Modal Sheet 3") }
}
@State private var showSheet4 = false
private var button4: some View {
Text("Sheet 4")
.onTapGesture { showSheet4 = true }
.sheet(isPresented: $showSheet4) { Text("Modal Sheet 4") }
}
}
struct OtherContentView_Previews: PreviewProvider {
static var previews: some View {
OtherContentView()
}
}
答案 10 :(得分:1)
公认的解决方案效果很好,但我想分享一个额外的补充,以防其他人遇到同样的问题。
我的问题
我遇到了两个按钮合二为一的问题。将两个按钮配对在一起,将整个 VStack
或 HStack
转换为一个大按钮。这仅允许一个 .sheet
触发,而不管是否使用了已接受的。
解决方案
answer here 对我来说就像拼图的缺失部分。
向每个按钮添加 .buttonStyle(BorderlessButtonStyle())
或 .buttonStyle(PlainButtonStyle())
,使它们像您期望的那样充当单个按钮。
抱歉,如果我在此处添加此内容犯了任何失礼,但这是我第一次在 StackOverlflow 上发帖。
答案 11 :(得分:1)
编辑:从 iOS 14.5 beta 3 开始,此问题现已修复:
SwiftUI 已在 iOS 和 iPadOS 14.5 Beta 3 中解决
with pdfplumber.open(file) as pdf:
pages = pdf.pages
for page in pdf.pages:
text = page.extract_text()
for i, line in enumerate(text.split('\n')):
print(i, line)
elif re.match(r"Error\s*:", line):
tot = line.split() # how can I get line on position i+2
和 sheet(isPresented:onDismiss:content:)
修饰符。 (74246633)在修复之前,一种解决方法是将工作表修改器应用于每个按钮:
fullScreenCover(item:onDismiss:content:)
由于工作表都做同样的事情,你可以将重复的功能提取到子视图中:
struct ContentView: View {
@State private var firstIsPresented = false
@State private var secondIsPresented = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
.sheet(isPresented: $secondIsPresented) {
Text("Second modal view")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
答案 12 :(得分:1)
此示例显示在同一ContentView
中使用4张纸,1个(或更多)警报和一个actionSheet的示例。在iOS 13,iOS 14中可以确定。在预览中可以确定
(来自注释:)目的是使用sheet(item:onDismiss:content:)
,并将项作为@State
var,并在枚举中定义值。这样,所有“业务”都是自包含在ContentView
中的。这样,工作表或警报的数量不受限制。
以下是以下代码的输出:
import SwiftUI
// exemple which show use of 4 sheets,
// 1 (or more) alerts,
// and an actionSheet in the same ContentView
// OK in iOS 13, iOS 14
// OK in Preview
// Any number of sheets, displayed as Views
// can be used for sheets in other views (with unique case values, of course)
enum SheetState {
case none
case AddItem
case PickPhoto
case DocPicker
case ActivityController
}
// Make Identifiable
extension SheetState: Identifiable {
var id: SheetState { self }
}
// the same for Alerts (who are not View, but Alert)
enum AlertState {
case none
case Delete
}
extension AlertState: Identifiable {
var id: AlertState { self }
}
struct ContentView: View {
// Initialized with nil value
@State private var sheetState: SheetState?
@State private var alertState: AlertState?
var body: some View {
NavigationView {
Form {
Text("Hello, world!")
Section(header: Text("sheets")) {
addItemButton
pickDocumentButton
pickPhoto
buttonExportView
}
Section(header: Text("alert")) {
confirmDeleteButton
}
Section(header: Text("Action sheet")) {
showActionSheetButton
}
}
.navigationTitle("Sheets & Alerts")
// ONLY ONE call .sheet(item: ... with required value in enum
// if item become not nil => display sheet
// when dismiss sheet (drag the modal view, or use presentationMode.wrappedValue.dismiss in Buttons) => item = nil
// in other way : if you set item to nil => dismiss sheet
// in closure, look for which item value display which view
// the "item" returned value contains the value passed in .sheet(item: ...
.sheet(item: self.$sheetState) { item in
if item == SheetState.AddItem {
addItemView // SwiftUI view
} else if item == SheetState.DocPicker {
documentPickerView // UIViewControllerRepresentable
} else if item == SheetState.PickPhoto {
imagePickerView // UIViewControllerRepresentable
} else if item == SheetState.ActivityController {
activityControllerView // UIViewControllerRepresentable
}
}
.alert(item: self.$alertState) { item in
if item == AlertState.Delete {
return deleteAlert
} else {
// Not used, but seem to be required
// .alert(item: ... MUST return an Alert
return noneAlert
}
}
}
}
// For cleaner contents : controls, alerts and sheet views are "stocked" in private var
// MARK: - Sheet Views
private var addItemView: some View {
Text("Add item").font(.largeTitle).foregroundColor(.blue)
// drag the modal view set self.sheetState to nil
}
private var documentPickerView: some View {
DocumentPicker() { url in
if url != nil {
DispatchQueue.main.async {
print("url")
}
}
self.sheetState = nil
// make the documentPicker view dismissed
}
}
private var imagePickerView: some View {
ImagePicker() { image in
if image != nil {
DispatchQueue.main.async {
self.logo = Image(uiImage: image!)
}
}
self.sheetState = nil
}
}
private var activityControllerView: some View {
ActivityViewController(activityItems: ["Message to export"], applicationActivities: [], excludedActivityTypes: [])
}
// MARK: - Alert Views
private var deleteAlert: Alert {
Alert(title: Text("Delete?"),
message: Text("That cant be undone."),
primaryButton: .destructive(Text("Delete"), action: { print("delete!") }),
secondaryButton: .cancel())
}
private var noneAlert: Alert {
Alert(title: Text("None ?"),
message: Text("No action."),
primaryButton: .destructive(Text("OK"), action: { print("none!") }),
secondaryButton: .cancel())
}
// In buttons, action set value in item for .sheet(item: ...
// Set self.sheetState value make sheet displayed
// MARK: - Buttons
private var addItemButton: some View {
Button(action: { self.sheetState = SheetState.AddItem }) {
HStack {
Image(systemName: "plus")
Text("Add an Item")
}
}
}
private var pickDocumentButton: some View {
Button(action: { self.sheetState = SheetState.DocPicker }) {
HStack {
Image(systemName: "doc")
Text("Choose Document")
}
}
}
@State private var logo: Image = Image(systemName: "photo")
private var pickPhoto: some View {
ZStack {
HStack {
Text("Pick Photo ->")
Spacer()
}
HStack {
Spacer()
logo.resizable().scaledToFit().frame(height: 36.0)
Spacer()
}
}
.onTapGesture { self.sheetState = SheetState.PickPhoto }
}
private var buttonExportView: some View {
Button(action: { self.sheetState = SheetState.ActivityController }) {
HStack {
Image(systemName: "square.and.arrow.up").imageScale(.large)
Text("Export")
}
}
}
private var confirmDeleteButton: some View {
Button(action: { self.alertState = AlertState.Delete}) {
HStack {
Image(systemName: "trash")
Text("Delete!")
}.foregroundColor(.red)
}
}
@State private var showingActionSheet = false
@State private var foregroundColor = Color.blue
private var showActionSheetButton: some View {
Button(action: { self.showingActionSheet = true }) {
HStack {
Image(systemName: "line.horizontal.3")
Text("Show Action Sheet")
}.foregroundColor(foregroundColor)
}
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Change foreground"), message: Text("Select a new color"), buttons: [
.default(Text("Red")) { self.foregroundColor = .red },
.default(Text("Green")) { self.foregroundColor = .green },
.default(Text("Blue")) { self.foregroundColor = .blue },
.cancel()
])
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
答案 13 :(得分:1)
我知道这个问题已经有很多答案,但是我发现了另一个非常有用的解决方案。它在这样的if语句中包裹工作表。对于操作表,我发现在iPad上的滚动视图中使用此处的其他解决方案(例如将每个工作表及其按钮包装在一个组中)通常会使操作表进入怪异的位置,因此此答案将解决滚动表中操作表的问题在iPad上观看。
struct ContentView: View{
@State var sheet1 = false
@State var sheet2 = false
var body: some View{
VStack{
Button(action: {
self.sheet1.toggle()
},label: {
Text("Sheet 1")
}).padding()
Button(action: {
self.sheet2.toggle()
},label: {
Text("Sheet 2")
}).padding()
}
if self.sheet1{
Text("")
.sheet(isPresented: self.$sheet1, content: {
Text("Some content here presenting sheet 1")
})
}
if self.sheet2{
Text("")
.sheet(isPresented: self.$sheet2, content: {
Text("Some content here presenting sheet 2")
})
}
}
}
答案 14 :(得分:0)
我认为这不是SwiftUI呈现任何视图的正确方法。
该范例通过创建在屏幕上显示某些内容的特定视图来工作,因此您可以在超级视图主体中拥有多个需要呈现某些内容的视图。因此,iOS 14上的SwiftUI 2将不会接受,开发人员应调用在某些情况下可以接受的超级视图中的所有演示,但是如果特定的视图呈现内容,那将有更好的时刻。
我为此实现了一个解决方案,并在iOS 14.1上使用Xcode 12.1在Swift 5.3上进行了测试
struct Presentation<Content>: View where Content: View {
enum Style {
case sheet
case popover
case fullScreenCover
}
@State private var isTrulyPresented: Bool = false
@State private var willPresent: Bool = false
@Binding private var isPresented: Bool
let content: () -> Content
let dismissHandler: (() -> Void)?
let style: Style
init(_ style: Style, _ isPresented: Binding<Bool>, onDismiss: (() -> Void)?, content: @escaping () -> Content) {
self._isPresented = isPresented
self.content = content
self.dismissHandler = onDismiss
self.style = style
}
@ViewBuilder
var body: some View {
if !isPresented && !willPresent {
EmptyView()
} else {
switch style {
case .sheet:
EmptyView()
.sheet(isPresented: $isTrulyPresented, onDismiss: dismissHandler, content: dynamicContent)
case .popover:
EmptyView()
.popover(isPresented: $isTrulyPresented, content: dynamicContent)
case .fullScreenCover:
EmptyView()
.fullScreenCover(isPresented: $isTrulyPresented, onDismiss: dismissHandler, content: dynamicContent)
}
}
}
}
extension Presentation {
var dynamicContent: () -> Content {
if isPresented && !isTrulyPresented {
OperationQueue.main.addOperation {
willPresent = true
OperationQueue.main.addOperation {
isTrulyPresented = true
}
}
} else if isTrulyPresented && !isPresented {
OperationQueue.main.addOperation {
isTrulyPresented = false
OperationQueue.main.addOperation {
willPresent = false
}
}
}
return content
}
}
之后,我可以为SwiftUI中的所有视图实现这些方法
public extension View {
func _sheet<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.sheet,
isPresented,
onDismiss: nil,
content: content
)
)
}
func _sheet<Content>(
isPresented: Binding<Bool>,
onDismiss: @escaping () -> Void,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.sheet,
isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
public extension View {
func _popover<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.popover,
isPresented,
onDismiss: nil,
content: content
)
)
}
}
public extension View {
func _fullScreenCover<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.fullScreenCover,
isPresented,
onDismiss: nil,
content: content
)
)
}
func _fullScreenCover<Content>(
isPresented: Binding<Bool>,
onDismiss: @escaping () -> Void,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.fullScreenCover,
isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
答案 15 :(得分:0)
我通过创建一个可观察的@State
来为我保存和管理状态,解决了SheetContext
和多张纸的混乱情况。然后,我只需要一个上下文实例,就可以告诉它以表格形式显示任何视图。
我在此博客文章中更详细地描述了它:https://danielsaidi.com/blog/2020/06/06/swiftui-sheets
答案 16 :(得分:0)
您的情况可以通过以下方法解决(已通过Xcode 11.2测试)
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
.sheet(isPresented: $secondIsPresented) {
Text("Only the second modal view works!")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
答案 17 :(得分:-1)
这次聚会有点晚了,但到目前为止,没有一个答案解决了让 viewModel 完成工作的可能性。由于我绝不是 SwiftUI 的专家(对它很陌生),完全有可能有更好的方法来做到这一点,但我找到的解决方案就在这里 -
enum ActiveSheet: Identifiable {
case first
case second
var id: ActiveSheet { self }
}
struct MyView: View {
@ObservedObject private var viewModel: MyViewModel
private var activeSheet: Binding<ActiveSheet?> {
Binding<ActiveSheet?>(
get: { viewModel.activeSheet },
set: { viewModel.activeSheet = $0 }
)
}
init(viewModel: MyViewModel) {
self.viewModel = viewModel
}
var body: some View {
HStack {
/// some views
}
.onTapGesture {
viewModel.doSomething()
}
.sheet(item: activeSheet) { _ in
viewModel.activeSheetView()
}
}
}
...在视图模型中 -
@Published var activeSheet: ActiveSheet?
func activeSheetView() -> AnyView {
switch activeSheet {
case .first:
return AnyView(firstSheetView())
case .second:
return AnyView(secondSheetView())
default:
return AnyView(EmptyView())
}
}
// call this from the view, eg, when the user taps a button
func doSomething() {
activeSheet = .first // this will cause the sheet to be presented
}
其中 firstSheetView() 和 secondSheetView() 提供所需的 actionSheet 内容。
我喜欢这种方法,因为它可以将所有业务逻辑排除在视图之外。