SwiftUI - 带有全屏模式的 PresentationButton
SwiftUI - PresentationButton with modal that is full screen
我正在尝试实现一个用 "Slide from Botton" 动画呈现另一个场景的按钮。
PresentationButton 看起来不错,所以我试了一下:
import SwiftUI
struct ContentView : View {
var body: some View {
NavigationView {
PresentationButton(destination: Green().frame(width: 1000.0)) {
Text("Click")
}.navigationBarTitle(Text("Navigation"))
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice("iPhone X")
.colorScheme(.dark)
ContentView()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)"
)
}
}
}
#endif
结果如下:
我希望绿色视图覆盖整个屏幕,并且模态不是"draggable to close"。
是否可以为 PresentationButton 添加修饰符以使其全屏且不可拖动?
我也试过导航按钮,但是:
- 它没有 "slide from bottom"
- 它在详细视图上创建了一个 "back button",我不想要
谢谢!
不幸的是,从 Beta 2Beta 3 开始,这在纯 SwiftUI 中是不可能的。可以看到 Modal
has no parameters for anything like UIModalPresentationStyle.fullScreen
. Likewise for PresentationButton.
我建议提交雷达。
您目前可以做的最接近的事情是:
@State var showModal: Bool = false
var body: some View {
NavigationView {
Button(action: {
self.showModal = true
}) {
Text("Tap me!")
}
}
.navigationBarTitle(Text("Navigation!"))
.overlay(self.showModal ? Color.green : nil)
}
当然,您可以从那里在叠加层中添加您喜欢的任何过渡。
虽然我的其他答案目前是正确的,但人们现在可能希望能够这样做。我们可以使用 Environment
将视图控制器传递给 children。 Gist here
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder { return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController ) }
}
extension EnvironmentValues {
var viewController: UIViewControllerHolder {
get { return self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
向 UIViewController 添加扩展
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) {
// Must instantiate HostingController with some sort of view...
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
// ... but then we can reset rootView to include the environment
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(value: toPresent))
)
self.present(toPresent, animated: true, completion: nil)
}
}
只要我们需要,就使用它:
struct MyView: View {
@Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
[EDIT] 虽然最好做类似 @Environment(\.viewController) var viewController: UIViewController?
的事情,但这会导致保留周期。因此,您需要使用支架。
所以我为此苦苦挣扎,我不喜欢覆盖功能和 ViewController 包装版本,因为它给我带来了一些内存错误,我对 iOS 很陌生,只知道SwiftUI,没有 UIKit。
我仅使用 SwiftUI 开发了 以下内容,这可能是叠加层所做的,但就我的目的而言,它更加灵活:
struct FullscreenModalView<Presenting, Content>: View where Presenting: View, Content: View {
@Binding var isShowing: Bool
let parent: () -> Presenting
let content: () -> Content
@inlinable public init(isShowing: Binding<Bool>, parent: @escaping () -> Presenting, @ViewBuilder content: @escaping () -> Content) {
self._isShowing = isShowing
self.parent = parent
self.content = content
}
var body: some View {
GeometryReader { geometry in
ZStack {
self.parent().zIndex(0)
if self.$isShowing.wrappedValue {
self.content()
.background(Color.primary.colorInvert())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height)
.transition(.move(edge: .bottom))
.zIndex(1)
}
}
}
}
}
正在为 View
添加扩展:
extension View {
func modal<Content>(isShowing: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) -> some View where Content: View {
FullscreenModalView(isShowing: isShowing, parent: { self }, content: content)
}
}
用法:
使用自定义视图并将 showModal
变量作为 Binding<Bool>
传递,以从视图本身中消除模式。
struct ContentView : View {
@State private var showModal: Bool = false
var body: some View {
ZStack {
Button(action: {
withAnimation {
self.showModal.toggle()
}
}, label: {
HStack{
Image(systemName: "eye.fill")
Text("Calibrate")
}
.frame(width: 220, height: 120)
})
}
.modal(isShowing: self.$showModal, content: {
Text("Hallo")
})
}
}
希望对您有所帮助!
你好 krjw
此版本修复了 XCode 11.1 中存在的编译错误,并确保控制器以传入的样式呈现。
import SwiftUI
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
self.present(toPresent, animated: true, completion: nil)
}
}
使用此版本,代码与上一版本无变化
struct MyView: View {
@Environment(\.viewController) private var viewControllerHolder: UIViewController?
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
我对此的解决方案(您可以轻松扩展以允许调整所呈现的 sheet 上的其他参数)是仅子class UIHostingController
//HSHostingController.swift
import Foundation
import SwiftUI
class HSHostingControllerParams {
static var nextModalPresentationStyle:UIModalPresentationStyle?
}
class HSHostingController<Content> : UIHostingController<Content> where Content : View {
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let nextStyle = HSHostingControllerParams.nextModalPresentationStyle {
viewControllerToPresent.modalPresentationStyle = nextStyle
HSHostingControllerParams.nextModalPresentationStyle = nil
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
在场景委托中使用 HSHostingController 而不是 UIHostingController
像这样:
// Use a HSHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
//This is the only change from the standard boilerplate
window.rootViewController = HSHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
然后在触发 sheet
之前告诉 HSHostingControllerParams class 你想要什么样的展示风格
.navigationBarItems(trailing:
HStack {
Button("About") {
HSHostingControllerParams.nextModalPresentationStyle = .fullScreen
self.showMenuSheet.toggle()
}
}
)
通过 class 单例传递参数感觉有点 'dirty',但在实践中 - 您必须创建一个非常模糊的场景才能使其无法按预期工作。
您可能会弄乱环境变量等(正如其他答案所做的那样)-但对我来说,增加的复杂性不值得纯粹。
更新:请参阅 this gist 以了解具有附加功能的扩展解决方案
Xcode 12.0 - SwiftUI 2 - iOS 14
现在可以了。使用 fullScreenCover() 修饰符。
var body: some View {
Button("Present!") {
self.isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
我正在尝试实现一个用 "Slide from Botton" 动画呈现另一个场景的按钮。
PresentationButton 看起来不错,所以我试了一下:
import SwiftUI
struct ContentView : View {
var body: some View {
NavigationView {
PresentationButton(destination: Green().frame(width: 1000.0)) {
Text("Click")
}.navigationBarTitle(Text("Navigation"))
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice("iPhone X")
.colorScheme(.dark)
ContentView()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)"
)
}
}
}
#endif
结果如下:
我希望绿色视图覆盖整个屏幕,并且模态不是"draggable to close"。
是否可以为 PresentationButton 添加修饰符以使其全屏且不可拖动?
我也试过导航按钮,但是: - 它没有 "slide from bottom" - 它在详细视图上创建了一个 "back button",我不想要
谢谢!
不幸的是,从 Beta 2Beta 3 开始,这在纯 SwiftUI 中是不可能的。可以看到 Modal
has no parameters for anything like UIModalPresentationStyle.fullScreen
. Likewise for PresentationButton.
我建议提交雷达。
您目前可以做的最接近的事情是:
@State var showModal: Bool = false
var body: some View {
NavigationView {
Button(action: {
self.showModal = true
}) {
Text("Tap me!")
}
}
.navigationBarTitle(Text("Navigation!"))
.overlay(self.showModal ? Color.green : nil)
}
当然,您可以从那里在叠加层中添加您喜欢的任何过渡。
虽然我的其他答案目前是正确的,但人们现在可能希望能够这样做。我们可以使用 Environment
将视图控制器传递给 children。 Gist here
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder { return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController ) }
}
extension EnvironmentValues {
var viewController: UIViewControllerHolder {
get { return self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
向 UIViewController 添加扩展
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) {
// Must instantiate HostingController with some sort of view...
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
// ... but then we can reset rootView to include the environment
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(value: toPresent))
)
self.present(toPresent, animated: true, completion: nil)
}
}
只要我们需要,就使用它:
struct MyView: View {
@Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
[EDIT] 虽然最好做类似 @Environment(\.viewController) var viewController: UIViewController?
的事情,但这会导致保留周期。因此,您需要使用支架。
所以我为此苦苦挣扎,我不喜欢覆盖功能和 ViewController 包装版本,因为它给我带来了一些内存错误,我对 iOS 很陌生,只知道SwiftUI,没有 UIKit。
我仅使用 SwiftUI 开发了
struct FullscreenModalView<Presenting, Content>: View where Presenting: View, Content: View {
@Binding var isShowing: Bool
let parent: () -> Presenting
let content: () -> Content
@inlinable public init(isShowing: Binding<Bool>, parent: @escaping () -> Presenting, @ViewBuilder content: @escaping () -> Content) {
self._isShowing = isShowing
self.parent = parent
self.content = content
}
var body: some View {
GeometryReader { geometry in
ZStack {
self.parent().zIndex(0)
if self.$isShowing.wrappedValue {
self.content()
.background(Color.primary.colorInvert())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height)
.transition(.move(edge: .bottom))
.zIndex(1)
}
}
}
}
}
正在为 View
添加扩展:
extension View {
func modal<Content>(isShowing: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) -> some View where Content: View {
FullscreenModalView(isShowing: isShowing, parent: { self }, content: content)
}
}
用法:
使用自定义视图并将 showModal
变量作为 Binding<Bool>
传递,以从视图本身中消除模式。
struct ContentView : View {
@State private var showModal: Bool = false
var body: some View {
ZStack {
Button(action: {
withAnimation {
self.showModal.toggle()
}
}, label: {
HStack{
Image(systemName: "eye.fill")
Text("Calibrate")
}
.frame(width: 220, height: 120)
})
}
.modal(isShowing: self.$showModal, content: {
Text("Hallo")
})
}
}
希望对您有所帮助!
你好 krjw
此版本修复了 XCode 11.1 中存在的编译错误,并确保控制器以传入的样式呈现。
import SwiftUI
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
self.present(toPresent, animated: true, completion: nil)
}
}
使用此版本,代码与上一版本无变化
struct MyView: View {
@Environment(\.viewController) private var viewControllerHolder: UIViewController?
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
我对此的解决方案(您可以轻松扩展以允许调整所呈现的 sheet 上的其他参数)是仅子class UIHostingController
//HSHostingController.swift
import Foundation
import SwiftUI
class HSHostingControllerParams {
static var nextModalPresentationStyle:UIModalPresentationStyle?
}
class HSHostingController<Content> : UIHostingController<Content> where Content : View {
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let nextStyle = HSHostingControllerParams.nextModalPresentationStyle {
viewControllerToPresent.modalPresentationStyle = nextStyle
HSHostingControllerParams.nextModalPresentationStyle = nil
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
在场景委托中使用 HSHostingController 而不是 UIHostingController 像这样:
// Use a HSHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
//This is the only change from the standard boilerplate
window.rootViewController = HSHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
然后在触发 sheet
之前告诉 HSHostingControllerParams class 你想要什么样的展示风格 .navigationBarItems(trailing:
HStack {
Button("About") {
HSHostingControllerParams.nextModalPresentationStyle = .fullScreen
self.showMenuSheet.toggle()
}
}
)
通过 class 单例传递参数感觉有点 'dirty',但在实践中 - 您必须创建一个非常模糊的场景才能使其无法按预期工作。
您可能会弄乱环境变量等(正如其他答案所做的那样)-但对我来说,增加的复杂性不值得纯粹。
更新:请参阅 this gist 以了解具有附加功能的扩展解决方案
Xcode 12.0 - SwiftUI 2 - iOS 14
现在可以了。使用 fullScreenCover() 修饰符。
var body: some View {
Button("Present!") {
self.isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}