【问题标题】:SwiftUI: Support multiple modalsSwiftUI:支持多种模式
【发布时间】:2019-11-27 22:29:48
【问题描述】:

我正在尝试设置一个可以根据点击的按钮显示多个模式的视图。

当我只添加一个sheet 时,一切正常:

.sheet(isPresented: $showingModal1) { ... }

但是当我添加另一张纸时,只有最后一张有效。

.sheet(isPresented: $showingModal1) { ... }
.sheet(isPresented: $showingModal2) { ... }

更新

我试图让它工作,但我不确定如何声明modal 的类型。我收到Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements 的错误。

struct ContentView: View {
    @State var modal: View?
    var body: some View {
        VStack {
            Button(action: {
                self.modal = ModalContentView1()
            }) {
                Text("Show Modal 1")
            }
            Button(action: {
                self.modal = ModalContentView2()
            }) {
                Text("Show Modal 2")
            }
        }.sheet(item: self.$modal, content: { modal in
            return modal
        })
    }
}

struct ModalContentView1: View {
    var body: some View {
        Text("Modal 1")
    }
}

struct ModalContentView2: View {
    var body: some View {
        Text("Modal 2")
    }
}

【问题讨论】:

  • @Alexander,你是怎么帮忙的?对于 OP,您的方法是否有好处……您是在问 beta 4 问题吗?发生了很大变化,您的代码表明了这一点。但是 - 对我来说,真正的问题(如果我准确的话)是你有没有在 beta 3 中工作的东西>
  • @dfd 前言:我不确定代码 1) 是否设置了 2 个不同的模型回调,在不同的时间由不同的事件触发,或者 2) 设置了模式回调,以及在回调上设置另一个模态回调,这会导致第二个模态显示在第一个模态上。当我写评论时,我认为它是#2,但现在回想起来,我认为它可能是#1,但我找不到关于它的好的文档
  • @dfd 考虑到我认为 #1 正在发生,并且 #1 将是糟糕的 UX 设计,我正在推动 OP 远离该选项,这会带来额外的副作用,即回避这个整个问题。
  • 我的意图是一次只显示一个模式。我在屏幕上有多个按钮,每个按钮都显示不同的模式。我在 Beta 3 中确实有这个工作,因为我可以将一个函数传递给.presentation,它将返回适当的模态以显示,或者如果不需要显示模态,则返回 nil。我现在似乎无法在 Beta 4 中找到执行此操作的方法。

标签: swift swiftui


【解决方案1】:

这行得通:

.background(EmptyView().sheet(isPresented: $showingModal1) { ... }
   .background(EmptyView().sheet(isPresented: $showingModal2) { ... }))

注意这些是如何嵌套的 backgrounds。不是两个一个接一个的背景。

感谢 DevAndArtist 找到这个。

【讨论】:

  • 太棒了!!像魅力一样工作!
  • 绝招,我不确定这是否应该是解决此问题的正确方法,但它保持如此干净。
  • 我认为这是最可靠的方法,特别是如果您有嵌套视图和它们自己的工作表修饰符。似乎视图树中最外层的工作表优先,因此在这种情况下使用 .sheet(item:) 也不起作用。
  • 在测试了许多不同的方法(即使用工作表修饰符的子视图、在工作表修饰符中切换大小写、在导航按钮中添加额外的工作表修饰符)之后,这似乎是功能性的,并且是 iOS 14 Beta 2 中最可靠的
  • 我在这个答案中写了一个小库,大大简化了语法github.com/davdroman/MultiSheet
【解决方案2】:

也许我错过了重点,但您可以通过一次调用 .sheet() 或多次调用来实现它。:

多个.sheet()方法:

import SwiftUI

struct MultipleSheets: View {
    @State private var sheet1 = false
    @State private var sheet2 = false
    @State private var sheet3 = false

    var body: some View {
        VStack {

            Button(action: {
                self.sheet1 = true
            }, label: { Text("Show Modal #1") })
            .sheet(isPresented: $sheet1, content: { Sheet1() })

            Button(action: {
                self.sheet2 = true
            }, label: { Text("Show Modal #2") })
            .sheet(isPresented: $sheet2, content: { Sheet2() })

            Button(action: {
                self.sheet3 = true
            }, label: { Text("Show Modal #3") })
            .sheet(isPresented: $sheet3, content: { Sheet3() })

        }
    }
}

struct Sheet1: View {
    var body: some View {
        Text("This is Sheet #1")
    }
}

struct Sheet2: View {
    var body: some View {
        Text("This is Sheet #2")
    }
}

struct Sheet3: View {
    var body: some View {
        Text("This is Sheet #3")
    }
}

单 .sheet() 方法:

struct MultipleSheets: View {
    @State private var showModal = false
    @State private var modalSelection = 1

    var body: some View {
        VStack {

            Button(action: {
                self.modalSelection = 1
                self.showModal = true
            }, label: { Text("Show Modal #1") })

            Button(action: {
                self.modalSelection = 2
                self.showModal = true
            }, label: { Text("Show Modal #2") })

            Button(action: {
                self.modalSelection = 3
                self.showModal = true
            }, label: { Text("Show Modal #3") })

        }
        .sheet(isPresented: $showModal, content: {
            if self.modalSelection == 1 {
                Sheet1()
            }

            if self.modalSelection == 2 {
                Sheet2()
            }

            if self.modalSelection == 3 {
                Sheet3()
            }
        })

    }
}

struct Sheet1: View {
    var body: some View {
        Text("This is Sheet #1")
    }
}

struct Sheet2: View {
    var body: some View {
        Text("This is Sheet #2")
    }
}

struct Sheet3: View {
    var body: some View {
        Text("This is Sheet #3")
    }
}

【讨论】:

  • 嗯,第一个解决方案对我不起作用。显示第一个模态后,其他按钮都无法显示其他模态
  • 这很奇怪。这个对我有用。您是否有机会在 NavigationView 中?或列表?我想我记得在那些条件下看到过这个问题。另外,第二种解决方案对您有用吗?
  • 这也适用于我。然而,问题似乎出在当您在父视图上调用.sheet() 时,该父视图具有已经调用.sheet() 的任何子视图。所以在@kontiki 发布的第一个示例中,在VStack 上调用.sheet() 不允许为Button 视图调用.sheet()
  • 这行得通,但似乎不是一个变通的解决方案。我从来没有想过在按钮本身上使用 .sheet 修饰符。
  • 这不再适用于 iOS 14。在第一种情况下,视图中的最后一个工作表修饰符将覆盖所有其他工作表(即第三个按钮工作表)。在第二种情况下,默认工作表(即 modalSelection 为 1)将在第一次点击按钮时加载,即使选择了 modalSelection 为 2。
【解决方案3】:

我不确定这是否总是可行的,但在 Xcode 11.3.1 中,对于这个用例 (https://developer.apple.com/documentation/swiftui/view/3352792-sheet) 存在 .sheet() 的重载。您可以使用 Identifiable item 而不是 bool 来调用它:

struct ModalA: View {

    var body: some View {
        Text("Hello, World! (A)")
    }

}

struct ModalB: View {

    var body: some View {
        Text("Hello, World! (B)")
    }

}

struct MyContentView: View {

    enum Sheet: Hashable, Identifiable {

        case a
        case b

        var id: Int {
            return self.hashValue
        }

    }

    @State var activeSheet: Sheet? = nil

    var body: some View {
        VStack(spacing: 42) {
            Button(action: {
                self.activeSheet = .a
            }) {
                Text("Hello, World! (A)")
            }
            Button(action: {
                self.activeSheet = .b
            }) {
                Text("Hello, World! (B)")
            }
        }
            .sheet(item: $activeSheet) { item in
                if item == .a {
                    ModalA()
                } else if item == .b {
                    ModalB()
                }
            }
    }

}

【讨论】:

  • 这很好用。非常感谢!将此与 iOS 13.4.1 和 Xcode 11.4.1 一起使用
  • 这是我使用的方法。效果很好。
  • 在上述逻辑的基础上,我正在呈现一些厌倦的动画
  • 这很好用,应该是公认的答案!作为奖励,如果您使用的是 Xcode 12,则可以使用 switch 语句而不是 if 语句来获取工作表的正确视图。
  • 我所做的唯一小改动是你不必遵守 Hashable 来遵守 Identifiable,所以我的枚举只是 enum Sheet: Identifiable 并且 id 属性只返回 self 和 @ 类型987654326@
【解决方案4】:

我个人会模仿一些 NavigationLink API。然后,您可以创建一个可散列枚举并决定您要呈现哪个模式表。

extension View {
  func sheet<Content, Tag>(
    tag: Tag,
    selection: Binding<Tag?>,
    content: @escaping () -> Content
  ) -> some View where Content: View, Tag: Hashable {
    let binding = Binding(
      get: {
        selection.wrappedValue == tag
      },
      set: { isPresented in
        if isPresented {
          selection.wrappedValue = tag
        } else {
          selection.wrappedValue = .none
        }
      }
    )
    return background(EmptyView().sheet(isPresented: binding, content: content))
  }
}

enum ActiveSheet: Hashable {
  case first
  case second
}

struct First: View {
  var body: some View {
    Text("frist")
  }
}

struct Second: View {
  var body: some View {
    Text("second")
  }
}

struct TestView: View {
  @State
  private var _activeSheet: ActiveSheet?

  var body: some View {
    print(_activeSheet as Any)
    return VStack
      {
        Button("first") {
          self._activeSheet = .first
        }
        Button("second") {
          self._activeSheet = .second
        }
      }
      .sheet(tag: .first, selection: $_activeSheet) {
        First()
      }
      .sheet(tag: .second, selection: $_activeSheet) {
        Second()
      }
  }
}

【讨论】:

    【解决方案5】:

    我在 plivesey 的回答中写了a library,大大简化了语法:

    .multiSheet {
        $0.sheet(isPresented: $sheetAPresented) { Text("Sheet A") }
        $0.sheet(isPresented: $sheetBPresented) { Text("Sheet B") }
        $0.sheet(isPresented: $sheetCPresented) { Text("Sheet C") }
    }
    

    【讨论】:

      【解决方案6】:

      我通过创建一个可观察的SheetContext 来解决这个问题,该SheetContext 持有和管理状态。然后我只需要一个上下文实例,并且可以告诉它以工作表的形式呈现任何视图。与“活动视图”绑定方法相比,我更喜欢这种方法,因为您可以通过多种方式使用此上下文。

      我在这篇博文中更详细地描述了它:https://danielsaidi.com/blog/2020/06/06/swiftui-sheets

      【讨论】:

        【解决方案7】:

        我想我找到了THE解决方案。它很复杂,所以这里是如何使用它的预告片:

        Button(action: {
            showModal.wrappedValue = ShowModal {
                AnyView( TheViewYouWantToPresent() )
            }
        })
        

        现在您可以在按钮级别定义您想要呈现的内容。呈现视图不需要知道任何东西。所以你在呈现视图上调用它。

        .background(EmptyView().show($showModal))
        

        我们在后台调用它,因此当$showModal 发生变化时,主视图不需要更新。

        好的,我们需要什么才能让它工作?

        1:ShowModal 类:

        public enum ModalType{
            case sheet, fullscreen
        }
        public struct ShowModal: Identifiable {
            public let id = ""
            public let modalType: ModalType
            public let content: () -> AnyView
            
            public init (modalType: ModalType = .sheet, @ViewBuilder content: @escaping () -> AnyView){
                self.modalType = modalType
                self.content = content
            }
        }
        

        忽略 id,我们只需要它用于 Identifiable。使用modalType,我们可以将视图呈现为工作表或全屏。内容是传递的视图,将显示在模态中。

        2:一个 ShowModal 绑定,它存储用于呈现视图的信息:

        @State var showModal: ShowModal? = nil
        

        我们需要将它添加到负责呈现的视图的环境中。所以我们可以轻松地在视图堆栈中访问它:

        VStack{
            InnerViewsThatWantToPresentModalViews()
        }
        .environment(\.showModal, $showModal)
        .background(EmptyView().show($showModal))
        

        在最后一行我们调用.show()。哪个负责演示。

        请记住,您必须创建 @State var showModal 并将其再次添加到环境中,该视图显示为模态并希望呈现另一个模态。

        4:要使用.show,我们需要扩展视图:

        public extension View {
            func show(_ modal: Binding<ShowModal?>) -> some View {
                modifier(VM_Show(modal))
            }
        }
        

        并添加一个viewModifier,处理$showModal传入的信息

        public struct VM_Show: ViewModifier {
            var modal: Binding<ShowModal?>
        
            public init(_ modal: Binding<ShowModal?>) {
                self.modal = modal
            }
            
            public func body(content: Content) -> some View {
                guard let modalType = modal.wrappedValue?.modalType else{ return AnyView(content) }
                switch modalType {
                case .sheet:
                    return AnyView(
                        content.sheet(item: modal){ modal in
                            modal.content()
                        }
                    )
                case .fullscreen:
                    return AnyView(
                        content.fullScreenCover(item: modal) { modal in
                            modal.content()
                        }
                    )
                }
            }
        }
        

        4:最后我们需要在想要呈现模态的视图中设置 showModal:

        获取变量:@Environment(\.showModal) var showModal。并这样设置:

        Button(action: {
            showModal.wrappedValue = ShowModal(modalType: .fullscreen) {
                AnyView( TheViewYouWantToPresent() )
            }
        })
        

        在定义 $showModal 的视图中,您设置它时不使用 WrappedValue:$showModal = ShowModal{...}

        【讨论】:

          【解决方案8】:

          作为替代方案,只需在布局中的某处放置一个清晰的像素可能对您有用:

          Color.clear.frame(width: 1, height: 1, alignment: .center).sheet(isPresented: $showMySheet, content: {
               MySheetView();
          })
          

          根据需要添加尽可能多的像素。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2015-08-18
            • 1970-01-01
            • 2011-05-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-12-08
            相关资源
            最近更新 更多