SwiftUI - PresentationButton with modal that is full screen

后端 未结 6 2071
滥情空心
滥情空心 2020-12-08 10:18

I am trying to implement a button that presents another scene with a \"Slide from Botton\" animation.

PresentationButton looked like a good candidate, so I gave it

相关标签:
6条回答
  • 2020-12-08 10:44

    Unfortunately, as of Beta 2 Beta 3, this is not possible in pure SwiftUI. You can see that Modal has no parameters for anything like UIModalPresentationStyle.fullScreen. Likewise for PresentationButton.

    I suggest filing a radar.

    The nearest you can currently do is something like:

        @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)
        }
    

    Of course, from there you can add whatever transition you like in the overlay.

    0 讨论(0)
  • 2020-12-08 10:50

    My solution for this (which you can easily extend to allow other params on the presented sheets to be tweaked) is to just subclass 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)
        }
    
    }
    

    use HSHostingController instead of UIHostingController in your scene delegate like so:

        // 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()
        }
    

    then just tell the HSHostingControllerParams class what presentation style you want before triggering a sheet

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

    Passing the params via the class singleton feels a little 'dirty', but in practice - you would have to create a pretty obscure scenario for this not to work as expected.

    You could mess around with environment variables and the like (as other answers have done) - but to me, the added complication isn't worth the purity.

    update: see this gist for extended solution with additional capabilities

    0 讨论(0)
  • 2020-12-08 10:53

    Although my other answer is currently correct, people probably want to be able to do this now. We can use the Environment to pass a view controller to 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 }
        }
    }
    

    Add an extension to 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)
        }
    }
    

    And whenever we need it, use it:

    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] Although it would be preferable to do something like @Environment(\.viewController) var viewController: UIViewController? this leads to a retain cycle. Therefore, you need to use the holder.

    0 讨论(0)
  • 2020-12-08 10:58

    So I was struggling with that and I didn't like the overlay feature nor the ViewController wrapped version since it gave me some memory bug and I am very new to iOS and only know SwiftUI and no UIKit.

    I developed credits the following with just SwiftUI which is probably what an overlay does but for my purposes it is much more flexible:

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

    Adding an extension to 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)
        }
    
    }
    

    Usage: Use a custom view and pass the showModal variable as a Binding<Bool> to dismiss the modal from the view itself.

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

    I hope this helps!

    Greetings krjw

    0 讨论(0)
  • 2020-12-08 11:01

    This version fixes the compile error present in XCode 11.1 as well as ensures that controller is presented in the style that is passed in.

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

    To use this version, the code is unchanged from the previous version.

    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!")
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-08 11:02

    Xcode 12.0 - SwiftUI 2 - iOS 14

    Now possible. Use fullScreenCover() modifier.

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

    Hacking With Swift

    0 讨论(0)
提交回复
热议问题