Skip to content

Instantly share code, notes, and snippets.

@fullc0de
Last active February 8, 2024 14:42
Show Gist options
  • Save fullc0de/3d68b6b871f20630b981c7b4d51c8373 to your computer and use it in GitHub Desktop.
Save fullc0de/3d68b6b871f20630b981c7b4d51c8373 to your computer and use it in GitHub Desktop.
UIKit style present in SwiftUI
//
// 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
}
}
@fullc0de
Copy link
Author

@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.

@KatkayApps
Copy link

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