Last active
February 8, 2024 14:42
-
-
Save fullc0de/3d68b6b871f20630b981c7b4d51c8373 to your computer and use it in GitHub Desktop.
UIKit style present in SwiftUI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// FullScreenPresent.swift | |
// | |
// Created by Heath Hwang on 5/16/20. | |
// | |
import SwiftUI | |
extension View { | |
/// This is used for presenting any SwiftUI view in UIKit way. As it uses some tricky way to make the objective, could possibly happen some issues at every upgrade of iOS version. | |
/// iOS 14 provides `.fullScreenCover` to support full screen presenting, it, however, seems not to work well if the SwiftUI view that has this modifier is contained in any UIKit view. | |
/// - After dismissal of a content view, all subviews of the content view will be reconstructed. | |
func uiKitFullPresent<V: View>(isPresented: Binding<Bool>, animated: Bool = true, transitionStyle: UIModalTransitionStyle = .coverVertical, presentStyle: UIModalPresentationStyle = .fullScreen, content: @escaping (_ dismissHandler: @escaping (_ completion: @escaping () -> Void) -> Void) -> V) -> some View { | |
self.modifier(FullScreenPresent(isPresented: isPresented, animated: animated, transitionStyle: transitionStyle, presentStyle: presentStyle, contentView: content)) | |
} | |
func uiKitFullPresent<V: View>(isPresented: Binding<Bool>, animated: Bool = true, customTransitionDelegate: UIViewControllerTransitioningDelegate, content: @escaping (_ dismissHandler: @escaping (_ completion: @escaping () -> Void) -> Void) -> V) -> some View { | |
self.modifier(FullScreenPresent(isPresented: isPresented, animated: animated, transitioningDelegate: customTransitionDelegate, contentView: content)) | |
} | |
} | |
struct FullScreenPresent<V: View>: ViewModifier { | |
typealias ContentViewBlock = (_ dismissHandler: @escaping (_ completion: @escaping () -> Void) -> Void) -> V | |
@Binding var isPresented: Bool | |
@State private var isAlreadyPresented: Bool = false | |
let animated: Bool | |
var transitionStyle: UIModalTransitionStyle = .coverVertical | |
var presentStyle: UIModalPresentationStyle = .fullScreen | |
let contentView: ContentViewBlock | |
private var transitioningDelegate: UIViewControllerTransitioningDelegate? = nil | |
init(isPresented: Binding<Bool>, animated: Bool, transitionStyle: UIModalTransitionStyle, presentStyle: UIModalPresentationStyle, contentView: @escaping ContentViewBlock) { | |
self._isPresented = isPresented | |
self.animated = animated | |
self.transitionStyle = transitionStyle | |
self.presentStyle = presentStyle | |
self.contentView = contentView | |
} | |
init(isPresented: Binding<Bool>, animated: Bool, transitioningDelegate: UIViewControllerTransitioningDelegate, contentView: @escaping ContentViewBlock) { | |
self._isPresented = isPresented | |
self.animated = animated | |
self.transitioningDelegate = transitioningDelegate | |
self.contentView = contentView | |
} | |
@ViewBuilder | |
func body(content: Content) -> some View { | |
if #available(iOS 14.0, *) { | |
contentIOS14(content) | |
} else { | |
contentDefault(content) | |
} | |
} | |
// Changed implementation | |
@available(iOS 14, *) | |
func contentIOS14(_ content: Content) -> some View { | |
content | |
.onChange(of: isPresented) { _ in | |
if isPresented { | |
Utils.print("[edit] presented appear") | |
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { | |
let topMost = UIViewController.topMost | |
let rootView = contentView({ [weak topMost] completion in | |
topMost?.dismiss(animated: animated) { | |
completion() | |
isPresented = false | |
} | |
}) | |
let hostingVC = UIHostingController(rootView: rootView) | |
if let customTransitioning = transitioningDelegate { | |
hostingVC.modalPresentationStyle = .custom | |
hostingVC.transitioningDelegate = customTransitioning | |
} else { | |
hostingVC.modalPresentationStyle = presentStyle | |
if presentStyle == .overFullScreen { | |
hostingVC.view.backgroundColor = .clear | |
} | |
hostingVC.modalTransitionStyle = transitionStyle | |
} | |
topMost?.present(hostingVC, animated: animated, completion: nil) | |
} | |
} | |
} | |
} | |
// Same as current implementation | |
func contentDefault(_ content: Content) -> some View { | |
Group { | |
if isPresented { | |
content | |
.onAppear { | |
Utils.print("[edit] presented appear") | |
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { | |
if isAlreadyPresented == false { | |
let topMost = UIViewController.topMost | |
let rootView = contentView({ [weak topMost] completion in | |
topMost?.dismiss(animated: animated) { | |
isPresented = false | |
isAlreadyPresented = false | |
completion() | |
} | |
}) | |
let hostingVC = UIHostingController(rootView: rootView) | |
if let customTransitioning = transitioningDelegate { | |
hostingVC.modalPresentationStyle = .custom | |
hostingVC.transitioningDelegate = customTransitioning | |
} else { | |
hostingVC.modalPresentationStyle = presentStyle | |
if presentStyle == .overFullScreen { | |
hostingVC.view.backgroundColor = .clear | |
} | |
hostingVC.modalTransitionStyle = transitionStyle | |
} | |
topMost?.present(hostingVC, animated: animated) { | |
isAlreadyPresented = true | |
} | |
} | |
} | |
} | |
} else { | |
content | |
} | |
} | |
} | |
} | |
extension UIViewController { | |
static var topMost: UIViewController? { | |
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first | |
if var topController = keyWindow?.rootViewController { | |
while let presentedViewController = topController.presentedViewController { | |
topController = presentedViewController | |
} | |
return topController | |
} | |
return nil | |
} | |
} |
Thank you for the solution. It really helped me.
Since iOS 13.0 is the minimum requirement for this code, you can use this code for the UIViewController.topMost
:
extension UIViewController {
static var topMost: UIViewController? {
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
return nil
}
}
@Mr-Alirezaa Thank you for your attention. I have put your code at the end of the gist. In addition, revised my code not to call dismiss method of presentedViewController
but to call topMost
's one. That's a common usage guided by Apple. You can check what's changed in details at Revisions
tab.
I still see a crash with the message
*** Terminating app due to uncaught exception 'SKUnsupportedPresentationException', reason: 'SKStoreProductViewController must be used in a modal view controller'
after the controller dismisses.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this code uses
UIViewController.topMost
. After posting, I noticed that i had missed to provide that code. but i didn't append it because that code is too long for this gist. I am sure that functionality can be easily implemented yourself or found in Web.