Call a function from SwiftUI in UIHostingController - ios

I have a swift UIHostingController which renders a SwiftUI. I call a function in view did appear which builds fine but doesn't create the intended output.
class LoginView: UIHostingController<LoginViewComponent> {
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: LoginViewComponent())
}
override func viewDidAppear(_ animated: Bool) {
sessionHandler()
}
func sessionHandler(){
let user = User()
if user.isLoggedIn(){
view.isUserInteractionEnabled = false
print("Authenticating Session")
self.rootView.loginState(state: "success")
}else{
view.isUserInteractionEnabled = true
print("Needs Logging in")
}
}
}
The function ("loginState(state: "success")") works when called within the SwiftUI view class, however when called from the hosting controller it doesn't work.
Any help would be greatly appreciated.

SwiftUI is reactive state-base machine, actually, and all views are struct values, so you need to change concept, instead of imperatively send messages specify state dependency and reactions on those states...
Thus, keeping your custom-host-controller it could be set up like the following
import SwiftUI
import UIKit
import Combine
// model that keeps login state
class LoginState: ObservableObject {
#Published var state: String = ""
}
struct LoginViewComponent: View {
#EnvironmentObject var loginState: LoginState // state to be observed
...
// somewhere in body you use loginState.state
// and view will be refreshed depending on state changes
}
class LoginView: UIHostingController<AnyView> {
let loginState = LoginState() // here it is created
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: AnyView(LoginViewComponent().environmentObject(self.loginState))) // here the ref injected
}
override func viewDidAppear(_ animated: Bool) {
sessionHandler()
}
func sessionHandler(){
let user = User()
if user.isLoggedIn(){
view.isUserInteractionEnabled = false
print("Authenticating Session")
self.loginState.state = "success" // here state changed
}else{
view.isUserInteractionEnabled = true
print("Needs Logging in")
}
}
}

Related

Swift: Can't dismiss view controller

I have a SwiftUI project in which I'm presenting a view controller to display an advertisement via MoPub.
Everything works as expected except one thing: when I tap the ad's close button, the ad itself closes but the black screen behind the ad continues to show. I guess the view controller is not being dismissed (but the completion block for dismiss does run).
Here's my code:
class InterstitialAds: UIViewController, MPInterstitialAdControllerDelegate {
var moPubView: MPInterstitialAdController?
func viewControllerForPresentingModalView() -> UIViewController! {
return self
}
//Called when you tap the ad's close button:
func interstitialWillDismiss(_ interstitial: MPInterstitialAdController) {
dismissControllerWithoutReward()
}
func showAd() {
let topViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController
self.modalPresentationStyle = .fullScreen
topViewController?.present(self, animated: true) {}
}
func dismissControllerWithoutReward() {
self.dismiss(animated: true) {
print("dismissControllerWithoutReward()") //Successfully prints to console
}
}
override func viewDidLoad() {
let adId = "4f117153f5c24fa6a3a92b818a5eb630" //Test ad unit
self.moPubView = MPInterstitialAdController(forAdUnitId: adId)
if let v = self.moPubView {
v.delegate = self
v.loadAd()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
v.show(from: self)
}
}
super.viewDidLoad()
}
}
Question:
Why isn't the view controller being dismissed, despite the successful call to dismiss?
Thank you!
Edit:
Interestingly, if I wait 0.5 seconds before trying to dismiss the view controller, it dismisses as desired. So, now I've got this code in interstitialWillDismiss(_:) (but I still want to know why this is happening):
func interstitialWillDismiss(_ interstitial: MPInterstitialAdController) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.dismissControllerWithoutReward()
}
}
For getting clear implementation and expected behavior you should wrap InterstitialAds controller in UIViewControllerRepresentable, then connect you SwiftUI side with some InterstitialAdsView which implements from UIViewControllerRepresentable via some isPresented flag binding.
Short example:
class InterstitialAds: UIViewController {
let onFlowCompleted: () -> Void
init(onFlowCompleted: #escaping () -> Void) {
self.onFlowCompleted = onFlowCompleted
// ...
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
onFlowCompleted()
// or somewhere else ...
}
}
struct HomeView: View {
#State var shouldShowInterstitialView = false
var body: some View {
Button {
shouldShowInterstitialView = true
} label: {
Text("Show Ad")
}
.fullScreenCover(isPresented: $shouldShowInterstitialView) {
InterstitialAdsView {
shouldShowInterstitialView = false
}
}
}
}
struct InterstitialAdsView: UIViewControllerRepresentable {
// #Environment(\.presentationMode) var presentationMode
// or
// #Binding var isPresented: Bool
// or
let onFlowCompleted: () -> Void
func makeUIViewController(context: Context) -> InterstitialAds {
InterstitialAds(onFlowCompleted: onFlowCompleted)
}
func updateUIViewController(_ uiViewController: InterstitialAds, context: Context) {
// update if needed
}
}

How to pass data from SwiftUI to ViewController

hoping someone can help me with this, I am building an iOS app with Swift and SwiftUI that launches different Unity scenes with the click of different buttons. I have the Unity part working well thanks to some good tutorials but I am having trouble being able to change scene with different button clicks. The tutorials I have followed set up the Unity code in a view controller that is then launched from a navigation view. I can launch the view controller and change the sceneNumber to whichever one I want and it will load that scene. I can't find away to do that in a way that is set before I launch the view controller.
Here is my View where the LauncherView has two buttons that will launch the view controller, I would like to change which sceneNumber is sent to the ViewController. I also have the problem of when I go back in the app and click through again nothing is displaying. I think this is because the UnityBridge class is already created and in the background somewhere.
Any help would be greatly appreciated! Thanks.
Here is my view code:
var sceneNumber: String = ""
struct MyViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
UnityBridge.getInstance().onReady = {
print("Unity is now ready!")
UnityBridge.getInstance().show(controller: vc)
let api = UnityBridge.getInstance().api
api.test(sceneNumber)
}
return vc
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
VStack{
MyViewController()
}
}
}
struct FirstView: View {
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: ContentView()) { //+ code to set sceneNumber to "1"
Text("Launch Unity Scene 1")
}
Text("")
NavigationLink(destination: ContentView()) { //+code to set sceneNumber to "2"
Text("Launch Unity Scene 2")
}
}
}
}
}
And here is the UnityBridge class code:
import Foundation
import UnityFramework
import SwiftUI
class API: NativeCallsProtocol {
internal weak var bridge: UnityBridge!
/**
Function pointers to static functions declared in Unity
*/
private var testCallback: TestDelegate!
/**
Public API
*/
public func test(_ value: String) {
self.testCallback(value)
}
/**
Internal methods are called by Unity
*/
internal func onUnityStateChange(_ state: String) {
switch (state) {
case "ready":
self.bridge.unityGotReady()
default:
return
}
}
internal func onSetTestDelegate(_ delegate: TestDelegate!) {
self.testCallback = delegate
}
}
class UnityBridge: UIResponder, UIApplicationDelegate, UnityFrameworkListener {
private static var instance : UnityBridge?
internal(set) public var isReady: Bool = false
public var api: API
private let ufw: UnityFramework
public var view: UIView? {
get { return self.ufw.appController()?.rootView }
}
public var onReady: () -> () = {}
public static func getInstance() -> UnityBridge {
print("Hello1.1")
//UnityBridge.instance?.unload()
if UnityBridge.instance == nil {
print("Hello1.2")
UnityBridge.instance = UnityBridge()
}
print("Hello1.3")
return UnityBridge.instance!
}
private static func loadUnityFramework() -> UnityFramework? {
print("Hello2")
let bundlePath: String = Bundle.main.bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: bundlePath)
if bundle?.isLoaded == false {
bundle?.load()
}
let ufw = bundle?.principalClass?.getInstance()
if ufw?.appController() == nil {
let machineHeader = UnsafeMutablePointer<MachHeader>.allocate(capacity: 1)
machineHeader.pointee = _mh_execute_header
ufw!.setExecuteHeader(machineHeader)
}
return ufw
}
internal override init() {
print("Hello3")
self.ufw = UnityBridge.loadUnityFramework()!
self.ufw.setDataBundleId("com.unity3d.framework")
self.api = API()
super.init()
self.api.bridge = self
self.ufw.register(self)
FrameworkLibAPI.registerAPIforNativeCalls(self.api)
ufw.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
}
public func show(controller: UIViewController) {
print("Hello4")
if self.isReady {
self.ufw.showUnityWindow()
}
if let view = self.view {
controller.view?.addSubview(view)
}
}
public func unload() {
print("Hello5")
self.ufw.unloadApplication()
}
internal func unityGotReady() {
print("Hello6")
self.isReady = true
onReady()
}
internal func unityDidUnload(_ notification: Notification!) {
print("Hello7")
ufw.unregisterFrameworkListener(self)
UnityBridge.instance = nil
}
}

iOS settings page helper class

I am trying to build a settings page helper class in order to simplify the setup of a settings page.
The idea would be that the class handles saving the state to UserDefaults and setting the initial state of any UISwitch.
Setting up a switch would just be a matter of setting a new switch to a class of "UISettingsSwitch" and adding the name of it to the accessibility label (it's the only identifier available as far as i'm aware).
So far I have :
import Foundation
import UIKit
class SettingsUISwitch: UISwitch {
enum SettingsType {
case darkMode, sound
}
func ison(type: SettingsType ) -> Bool {
switch type {
case .darkMode:
return userDefaults.bool(forKey: "darkMode")
case .sound:
return userDefaults.bool(forKey: "sound")
}
}
let userDefaults = UserDefaults.standard
override init(frame: CGRect) {
super.init(frame: frame)
initSwitch()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSwitch()
}
deinit {
}
func initSwitch() {
addTarget(self, action: #selector(toggle), for: .valueChanged)
}
#objc func toggle(){
userDefaults.setValue(self.isOn, forKey: self.accessibilityLabel!)
}
}
Not an awful lot I know.
I can currently do :
if settingsSwitch.ison(type: .darkMode) {
print (settingsSwitch.ison(type: .darkMode))
print ("ON")
} else {
print ("OFF")
}
The accessibility label doesn't seem to be available in the init setup at any point, so setting up the initial state doesn't seem to be a possibility.
Is it possible to set the initial state of the UISwitch this way ?
Ideally , I'd like to expose : settingsSwitch.darkMode.ison as a boolean ... but I can't figure that one out. Thanks for any help
I managed to use the restoration identifier to do the setup for the switch but I'd still love to remove the cases and the repeated calls to userDefaults
import Foundation
import UIKit
class UISwitchSettings: UISwitch {
enum SettingsType: String, CaseIterable {
case darkMode = "darkMode"
case sound = "sound"
}
func ison(type: SettingsType ) -> Bool {
switch type {
case .darkMode:
return userDefaults.bool(forKey: "darkMode")
case .sound:
return userDefaults.bool(forKey: "sound")
}
}
let userDefaults = UserDefaults.standard
override init(frame: CGRect) {
super.init(frame: frame)
initSwitch()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSwitch()
}
deinit {
}
func initSwitch() {
if let key = self.restorationIdentifier {
// Logic needs changing if default switch is off
if userDefaults.bool(forKey: key) || userDefaults.object(forKey: key) == nil {
self.isOn = true
} else {
self.isOn = false
}
}
addTarget(self, action: #selector(toggle), for: .valueChanged)
}
#objc func toggle(){
userDefaults.setValue(self.isOn, forKey: self.restorationIdentifier!)
}
}

Delegate always getting nil value in dynamic framework class

I used delegation for passing data to ViewController B to A in dynamic framework . B is my dynamic framework ViewController . A is my app ViewController . I am always set delegate as self in my A class
Without dynamic framework it works perfectly
Class B code : Inside dynamic framework (Using .xib)
import UIKit
public protocol MediaDataDelegate: class{
func mediaDidFinish(controller:
LoginViewController,transactionId:String,returnURL: String)
}
public class LoginViewController: UIViewController {
public var message = ""
public var delegate: MediaDataDelegate?
public init() {
super.init(nibName: "LoginViewController", bundle: Bundle(for: LoginViewController.self))
print("message 1234 :\(message)")
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func viewDidLoad() {
super.viewDidLoad()
print("message 1234 :\(message)")
}
public class func logToConsole(_ msg: String) {
print(msg);
}
#IBAction func backToMainBtnTapped(_ sender: UIButton) {
self.delegate?.mediaDidFinish(controller: self, transactionId: "WERTYQWRCT", returnURL: "www.media.com")
}
}
Class A Code:Inside Other App (Using Storyboard)
Click on conduct IPVButton navigate to dynamic framework view controller
I also pass some value to message string but in dynamic framework class getting empty string.
import UIKit
import NBView
class ViewController: UIViewController ,MediaDataDelegate{
var loginVC = LoginViewController()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
LoginViewController.logToConsole("hello media")
}
#IBAction func conductIPVBtnTapped(_ sender: Any) {
loginVC.delegate = self
present(loginVC, animated: true, completion: nil)
}
func mediaDidFinish(controller: LoginViewController, transactionId:
String, returnURL: String) {
print("Trans Id\(transactionId)")
print("return URl \(returnURL)")
}
}
It is because you are instantiating incorrectly the LoginViewController
You need to do it this way, since you wrote that you have it in a .xib file:
let loginVC = LoginViewController(nibName: yourNibName, bundle: nil)
Always have a weak reference to your delegate, otherwise you will have a retain cycle:
weak var delegate: MediaDataDelegate?
Also, you don't need to use public everywhere where you thought it might fit. Use it wisely and when needed. Here you don't need it. Remove it from everywhere in your LoginViewController

How to hide the home indicator with SwiftUI?

What's the UIKit equivalent of the prefersHomeIndicatorAutoHidden property in SwiftUI?
Since I could't find this in the default API either, I made it myself in a subclass of UIHostingController.
What I wanted:
var body: some View {
Text("I hide my home indicator")
.prefersHomeIndicatorAutoHidden(true)
}
Since the prefersHomeIndicatorAutoHidden is a property on UIViewController we can override that in UIHostingController but we need to get the prefersHomeIndicatorAutoHidden setting up the view hierarchy, from our view that we set it on to the rootView in UIHostingController.
The way that we do that in SwiftUI is PreferenceKeys. There is lots of good explanation on that online.
So what we need is a PreferenceKey to send the value up to the UIHostingController:
struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey {
typealias Value = Bool
static var defaultValue: Value = false
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue() || value
}
}
extension View {
// Controls the application's preferred home indicator auto-hiding when this view is shown.
func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View {
preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value)
}
}
Now if we add .prefersHomeIndicatorAutoHidden(true) on a View it sends the PrefersHomeIndicatorAutoHiddenPreferenceKey up the view hierarchy. To catch that in the hosting controller I made a subclass that wraps the rootView to listen to the preference change, then update the UIViewController.prefersHomeIndicatorAutoHidden:
// Not sure if it's bad that I cast to AnyView but I don't know how to do this with generics
class PreferenceUIHostingController: UIHostingController<AnyView> {
init<V: View>(wrappedView: V) {
let box = Box()
super.init(rootView: AnyView(wrappedView
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
box.value?._prefersHomeIndicatorAutoHidden = $0
}
))
box.value = self
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private class Box {
weak var value: PreferenceUIHostingController?
init() {}
}
// MARK: Prefers Home Indicator Auto Hidden
private var _prefersHomeIndicatorAutoHidden = false {
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
}
override var prefersHomeIndicatorAutoHidden: Bool {
_prefersHomeIndicatorAutoHidden
}
}
Full example that doesn't expose the PreferenceKey type and has preferredScreenEdgesDeferringSystemGestures too on git: https://gist.github.com/Amzd/01e1f69ecbc4c82c8586dcd292b1d30d
For SwiftUI with the new application life cycle
From SwiftUI 2.0 when using the new Application Life Cycle we need to create a new variable in our #main .app file with the wrapper:
#UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
The main app file will look like this:
import SwiftUI
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Then we create our UIApplicationDelegate class in a new file:
import UIKit
class MyAppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let config = UISceneConfiguration(name: "My Scene Delegate", sessionRole: connectingSceneSession.role)
config.delegateClass = MySceneDelegate.self
return config
}
}
Above we passed the name of our SceneDelegate class as "MySceneDelegate", so lets create this class in a separate file:
class MySceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let rootView = ContentView()
let hostingController = HostingController(rootView: rootView)
window.rootViewController = hostingController
self.window = window
window.makeKeyAndVisible()
}
}
}
The property prefersHomeIndicatorAutoHidden will have to be overridden in the HostingController class as usual as in the above solution by ShengChaLover:
class HostingController: UIHostingController<ContentView> {
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
Of course do not forget to replace contentView with the name of your view if different!
Kudos to Paul Hudson of Hacking with Swift and Kilo Loco for the hints!
The only solution i found to work 100% of the time was swizzling the instance property 'prefersHomeIndicatorAutoHidden' in all UIViewControllers that way it always returned true.
Create a extension on NSObject for swizzling instance methods / properties
//NSObject+Swizzle.swift
extension NSObject {
class func swizzle(origSelector: Selector, withSelector: Selector, forClass: AnyClass) {
let originalMethod = class_getInstanceMethod(forClass, origSelector)
let swizzledMethod = class_getInstanceMethod(forClass, withSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}
Created extension on UIViewController this will swap the instance property in all view controller with one we created that always returns true
//UIViewController+HideHomeIndicator.swift
extension UIViewController {
#objc var swizzle_prefersHomeIndicatorAutoHidden: Bool {
return true
}
public class func swizzleHomeIndicatorProperty() {
self.swizzle(origSelector:#selector(getter: UIViewController.prefersHomeIndicatorAutoHidden),
withSelector:#selector(getter: UIViewController.swizzle_prefersHomeIndicatorAutoHidden),
forClass:UIViewController.self)
}
}
Then call swizzleHomeIndicatorProperty() function in your App Delegate
// AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//Override 'prefersHomeIndicatorAutoHidden' in all UIViewControllers
UIViewController.swizzleHomeIndicatorProperty()
return true
}
}
if using SwiftUI register your AppDelegate using UIApplicationDelegateAdaptor
//Application.swift
#main
struct Application: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
iOS 16
you can use the .persistentSystemOverlays and pass in .hidden to hide all non-transient system views that are automatically placed over our UI
Text("Goodbye home indicator, the multitask indicator on iPad, and more.")
.persistentSystemOverlays(.hidden)
I have managed to hide the Home Indicator in my single view app using a technique that's simpler than what Casper Zandbergen proposes. It's way less 'generic' and I am not sure the preference will propagate down the view hierarchy, but in my case that's just enough.
In your SceneDelegate subclass the UIHostingController with your root view type as the generic parameter and override prefersHomeIndicatorAutoHidden property.
class HostingController: UIHostingController<YourRootView> {
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
In the scene method's routine create an instance of you custom HostingController passing the root view as usual and assign that instance to window's rootViewController:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let rootView = YourRootView()
let hostingController = HostingController(rootView: rootView)
window.rootViewController = hostingController
self.window = window
window.makeKeyAndVisible()
}
Update: this will not work if you need to inject an EnvironmentObject into a root view.
My solution is made for one screen only (UIHostingController). It means you do not need to replace UIHostingController in the whole app and deal with AppDelegate. Thus it will not affect injection of your EnvironmentObjects into ContentView. If you want to have just one presented screen with hideable home indicator, you need to wrap your view around custom UIHostingController and present it.
This can be done so (or you can also use PreferenceUIHostingController like in previous answers if you want to change the property in runtime. But I guess it will require some more workarounds):
final class HomeIndicatorHideableHostingController: UIHostingController<AnyView> {
init<V: View>(wrappedView: V) {
super.init(rootView: AnyView(wrappedView))
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
Then you have to present your HomeIndicatorHideableHostingController in
UIKit style (tested on iOS 14). The solution is based on this: https://gist.github.com/fullc0de/3d68b6b871f20630b981c7b4d51c8373. If you want to adapt it to iOS 13 look through the link (topMost property is also found there).
You create view modifier for it just like fullScreenCover:
public 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.
/// This way of presentation allows to present view in a custom `UIHostingController`
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 {
modifier(FullScreenPresent(isPresented: isPresented,
animated: animated,
transitionStyle: transitionStyle,
presentStyle: presentStyle,
contentView: content))
}
}
Modifer itself:
public struct FullScreenPresent<V: View>: ViewModifier {
typealias ContentViewBlock = (_ dismissHandler: #escaping (_ completion: #escaping () -> Void) -> Void) -> V
#Binding var isPresented: Bool
let animated: Bool
var transitionStyle: UIModalTransitionStyle = .coverVertical
var presentStyle: UIModalPresentationStyle = .fullScreen
let contentView: ContentViewBlock
private weak var transitioningDelegate: UIViewControllerTransitioningDelegate?
init(isPresented: Binding<Bool>,
animated: Bool,
transitionStyle: UIModalTransitionStyle,
presentStyle: UIModalPresentationStyle,
contentView: #escaping ContentViewBlock) {
_isPresented = isPresented
self.animated = animated
self.transitionStyle = transitionStyle
self.presentStyle = presentStyle
self.contentView = contentView
}
#ViewBuilder
public func body(content: Content) -> some View {
content
.onChange(of: isPresented) { _ in
if isPresented {
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 = HomeIndicatorHideableHostingController(wrappedView: 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)
}
}
}
}
}
And then you use it like this:
struct ContentView: View {
#State var modalPresented: Bool = false
var body: some View {
Button(action: {
modalPresented = true
}) {
Text("First view")
}
.uiKitFullPresent(isPresented: $modalPresented) { closeHandler in
SomeModalView(close: closeHandler)
}
}
}
struct SomeModalView: View {
var close: (#escaping () -> Void) -> Void
var body: some View {
Button(action: {
close({
// Do something when dismiss animation finished
})
}) {
Text("Tap to go back")
}
}
}

Resources