SwiftUI - 使用全屏模态视图的PresentationButton

30

我正在尝试实现一个按钮,该按钮可以呈现另一个场景,并使用“从底部滑动”的动画效果。

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

这是结果: 在这里输入图像描述

我希望绿色视图覆盖整个屏幕,而且模态框不可被“拖动关闭”。

是否可能将修改器添加到 PresentationButton 使其全屏且不可拖动?

我还尝试了一个导航按钮,但是: - 它不会“从底部滑动” - 它会创建一个“返回按钮”在详细视图上,我不想要它

谢谢!

6个回答

24

很遗憾,在Beta 3版本之前,使用纯SwiftUI实现这一点是不可能的。你可以看到,Modal 没有任何参数,比如UIModalPresentationStyle.fullScreen。同样适用于PresentationButton

我建议提交一个radar。

目前你能做到的最接近的方法是:

    @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)
    }
当然,从那里您可以在覆盖层中添加任何您喜欢的过渡效果。

20

尽管我之前的回答目前是正确的,但人们可能希望现在就能够做到这一点。我们可以使用Environment将视图控制器传递给子级。 此处有代码片段

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!")
        }
    }
}

[编辑]虽然像@Environment(\.viewController) var viewController: UIViewController?这样做可能更好,但这会导致保留循环。因此,您需要使用holder。


这段代码有错误吗?因为我已经在Beta 6上尝试使用了你的gist,在MyView内部的self.viewControllerHolder.value行处,编译器报错Ambiguous reference to member 'value(forKey:)',所以我尝试使用self.viewControllerHolder?.value(forKey: "") as? UIViewController,但它崩溃并显示错误valueForKey:]: this class is not key value coding-compliant for the key .' - riciloma
1
搞定了:只需从 self.viewControllerHolder.value 中删除 .value。但是在 Beta 7 中,它转换为标准的 .sheet 模态视图,所以不是真正的全屏。 - riciloma
2
我建议在 extensionpresent 函数中添加 toPresent.modalPresentationStyle = style,以使新呈现的屏幕实际上是全屏。 - riciloma
@arsenius,这段代码对于2019年12月有什么更新吗?我得到了一个“无法将类型为'Environment<ViewControllerHolder>'的值转换为指定类型'UIViewController?'”的错误,针对@Environment(\.viewController) private var viewControllerHolder: UIViewController? - Steven Schafer
@StevenSchafer 那应该是 ... var viewControllerHolder: ViewControllerHolder。我已经修复了我的帖子。 - arsenius
显示剩余2条评论

12

Xcode 12.0 - SwiftUI 2 - iOS 14

现在可以使用 fullScreenCover() 修饰符了。

var body: some View {
    Button("Present!") {
        self.isPresented.toggle()
    }
    .fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}

Hacking With Swift


3
我的解决方案是(您可以轻松扩展以允许调整呈现表格上的其他参数)只需子类化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()
    }

然后只需在触发表单之前告诉HSHostingControllerParams类您想要的演示样式

        .navigationBarItems(trailing:
            HStack {
                Button("About") {
                    HSHostingControllerParams.nextModalPresentationStyle = .fullScreen
                    self.showMenuSheet.toggle()
                }
            }
        )

通过类单例传递参数感觉有点“不干净”,但实际上,除非你创建一个相当模糊的场景,否则这种方法都能正常工作。你可以像其他答案一样搞环境变量之类的东西,但对我来说,增加的复杂性并不值得纯洁性。更新:请参见this gist以获取具有附加功能的扩展解决方案。

3

此版本修复了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!")
        }
    }
}

我尝试了这段代码,但它不起作用。一开始你会得到一个错误:Value of optional type 'UIViewController?' must be unwrapped to refer to member 'value' of wrapped base type 'UIViewController',但是强制解包之后又出现了另一个错误:Ambiguous reference to member 'value(forKey:)'。因此,你需要将 self.viewControllerHolder.value 改为 self.viewControllerHolder! - codewithfeeling

2

我一直在苦恼,不喜欢覆盖功能和ViewController包装版本,因为它们会导致一些内存错误。我是iOS的新手,只了解SwiftUI而不了解UIKit。

我使用了SwiftUI开发了以下内容credits,它可能就是覆盖功能,但对于我的目的来说更加灵活:

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

2
它有一些限制,必须将它放置在顶部视图上,而不是嵌套在视图层次结构中的视图上,因此它的工作方式与表格不同。 - Michał Ziobro
你是指这个还是 .sheet - krjw
如果您将此修饰符附加到按钮中的视图嵌套中,并将其嵌套在另一个视图中,则它将居中于此按钮或子视图的中心。也许可以根据UIScreen大小明确地设置框架、位置。 - Michał Ziobro
我认为计算屏幕中心和父级中心之间的差异,并将其设置为偏移量可能会有所帮助。 - Michał Ziobro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接