I am upgrading my app to use the new UIScene patterns as defined in iOS 13, however a critical part of the app has stopped working.
I have been using a UIWindow to cover the current content on the screen and present new information to the user, but in the current beta I am working with (iOS + XCode beta 3) the window will appear, but then disappear straight away.
Here is the code I was using, that now does not work:
let window = UIWindow(frame: UIScreen.main.bounds)
let viewController = UIViewController()
viewController.view.backgroundColor = .clear
window.rootViewController = viewController
window.windowLevel = UIWindow.Level.statusBar + 1
window.makeKeyAndVisible()
viewController.present(self, animated: true, completion: nil)
I have tried many things, including using WindowScenes to present the new UIWindow, but cannot find any actual documentation or examples out there.
One of my attempts (Did not work - same behaviour with window appearing and dismissing straight away)
let windowScene = UIApplication.shared.connectedScenes.first
if let windowScene = windowScene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let viewController = UIViewController()
viewController.view.backgroundColor = .clear
window.rootViewController = viewController
window.windowLevel = UIWindow.Level.statusBar + 1
window.makeKeyAndVisible()
viewController.present(self, animated: true, completion: nil)
}
Has anyone been able to do this yet in iOS 13 beta?
Thanks
EDIT
Some time has passed between asking this and the final version of iOS 13 being released. There are a lot of answers below, but almost all of them include one thing - Adding a strong/stronger reference to the UIWindow. You may need to include some code relating the the new Scenes, but try adding the strong reference first.
I was experiencing the same problems while upgrading my code for iOS 13 scenes pattern. With parts of your second code snippet I managed to fix everything so my windows are appearing again. I was doing the same as you except for the last line. Try removing viewController.present(...). Here's my code:
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
if let windowScene = windowScene as? UIWindowScene {
popupWindow = UIWindow(windowScene: windowScene)
}
Then I present it like you do:
popupWindow?.frame = UIScreen.main.bounds
popupWindow?.backgroundColor = .clear
popupWindow?.windowLevel = UIWindow.Level.statusBar + 1
popupWindow?.rootViewController = self as? UIViewController
popupWindow?.makeKeyAndVisible()
Anyway, I personally think that the problem is in viewController.present(...), because you show a window with that controller and immediately present some 'self', so it depends on what 'self' really is.
Also worth mentioning that I store a reference to the window you're moving from inside my controller. If this is still useless for you I can only show my small repo that uses this code. Have a look inside AnyPopupController.swift and Popup.swift files.
Hope that helps, #SirOz
Based on all the proposed solutions, I can offer my own version of the code:
private var window: UIWindow!
extension UIAlertController {
func present(animated: Bool, completion: (() -> Void)?) {
window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIViewController()
window.windowLevel = .alert + 1
window.makeKeyAndVisible()
window.rootViewController?.present(self, animated: animated, completion: completion)
}
open override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
window = nil
}
}
How to use:
// Show message (from any place)
let alert = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Button", style: .cancel))
alert.present(animated: true, completion: nil)
Here are the steps to present a view controller in a new window on iOS 13:
Detect focused UIWindowScene.
extension UIWindowScene {
static var focused: UIWindowScene? {
return UIApplication.shared.connectedScenes
.first { $0.activationState == .foregroundActive && $0 is UIWindowScene } as? UIWindowScene
}
}
Create UIWindow for the focused scene.
if let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) {
// ...
}
Present UIViewController in that window.
let myViewController = UIViewController()
if let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) {
window.rootViewController = myViewController
window.makeKeyAndVisible()
}
You just need to store strong reference of UIWindow that you want to present. It seems that under the hood view controller that presented does not references to the window.
Thank you #glassomoss. My problem is with UIAlertController.
I solved my problem in this way:
I added a variable
var windowsPopUp: UIWindow?
I modified the code to display the PopUp:
public extension UIAlertController {
func showPopUp() {
windowsPopUp = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
vc.view.backgroundColor = .clear
windowsPopUp!.rootViewController = vc
windowsPopUp!.windowLevel = UIWindow.Level.alert + 1
windowsPopUp!.makeKeyAndVisible()
vc.present(self, animated: true)
}
}
In the action of the UIAlertController I added:
windowsPopUp = nil
without the last line the PopUp is dismissed but the windows remains active not allowing the iteration with the application (with the application window)
As everyone else mentioned, the issue is that a strong reference to the window is required. So to make sure that this window is removed again after use, I encapsulated everything needed in it's own class..
Here's a little Swift 5 snippet:
class DebugCheatSheet {
private var window: UIWindow?
func present() {
let vc = UIViewController()
vc.view.backgroundColor = .clear
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = vc
window?.windowLevel = UIWindow.Level.alert + 1
window?.makeKeyAndVisible()
vc.present(sheet(), animated: true, completion: nil)
}
private func sheet() -> UIAlertController {
let alert = UIAlertController.init(title: "Cheatsheet", message: nil, preferredStyle: .actionSheet)
addAction(title: "Ok", style: .default, to: alert) {
print("Alright...")
}
addAction(title: "Cancel", style: .cancel, to: alert) {
print("Cancel")
}
return alert
}
private func addAction(title: String?, style: UIAlertAction.Style, to alert: UIAlertController, action: #escaping () -> ()) {
let action = UIAlertAction.init(title: title, style: style) { [weak self] _ in
action()
alert.dismiss(animated: true, completion: nil)
self?.window = nil
}
alert.addAction(action)
}
}
And here is how I use it.. It's from the lowest view controller in the whole apps view hierarchy, but could be used from anywhere else also:
private let cheatSheet = DebugCheatSheet()
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
cheatSheet.present()
}
}
iOS 13 broke my helper functions for managing alerts.
Because there may be cases where you need multiple alerts to be displayed at the same time (the most recent above the older) for example in case you're displaying a yes or no alert and in the meanwhile your webservice returns with a error you display via an alert (it's a limit case but it can happen),
my solution is to extend the UIAlertController like this, and let it have its own alertWindow to be presented from.
The pro is that when you dismiss the alert the window is automatically dismissed because there is any strong reference left, so no further mods to be implemented.
Disclaimer : I just implemented it, so I still need to see if it's consistent...
class AltoAlertController: UIAlertController {
var alertWindow : UIWindow!
func show(animated: Bool, completion: (()->(Void))?)
{
alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindow.Level.alert + 1
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(self, animated: animated, completion: completion)
}
}
Here's a bit hacky way of holding a strong reference to created UIWindow and releasing it after presented view controller is dismissed and deallocated.
Just make sure you don't make reference cycles.
private final class WindowHoldingViewController: UIViewController {
private var window: UIWindow?
convenience init(window: UIWindow) {
self.init()
self.window = window
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.clear
}
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
let view = DeallocatingView()
view.onDeinit = { [weak self] in
self?.window = nil
}
viewControllerToPresent.view.addSubview(view)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
private final class DeallocatingView: UIView {
var onDeinit: (() -> Void)?
deinit {
onDeinit?()
}
}
}
Usage:
let vcToPresent: UIViewController = ...
let window = UIWindow() // or create via window scene
...
window.rootViewController = WindowHoldingViewController(window: window)
...
window.rootViewController?.present(vcToPresent, animated: animated, completion: completion)
Need have pointer a created window for ios13.
example my code:
extension UIAlertController {
private static var _aletrWindow: UIWindow?
private static var aletrWindow: UIWindow {
if let window = _aletrWindow {
return window
} else {
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIViewController()
window.windowLevel = UIWindowLevelAlert + 1
window.backgroundColor = .clear
_aletrWindow = window
return window
}
}
func presentGlobally(animated: Bool, completion: (() -> Void)? = nil) {
UIAlertController.aletrWindow.makeKeyAndVisible()
UIAlertController.aletrWindow.rootViewController?.present(self, animated: animated, completion: completion)
}
open override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
UIAlertController.aletrWindow.isHidden = true
}
}
use:
let alert = UIAlertController(...
...
alert.presentGlobally(animated: true)
You can try like this:
extension UIWindow {
static var key: UIWindow? {
if #available(iOS 13, *) {
return UIApplication.shared.windows.first { $0.isKeyWindow }
} else {
return UIApplication.shared.keyWindow
}
}
}
Usage:
if let rootVC = UIWindow.key?.rootViewController {
rootVC.present(nextViewController, animated: true, completion: nil)
}
Keep Coding........ :)
Swift 4.2 iOS 13 UIAlertController extension
This code full working in iOS 11, 12 and 13
import Foundation
import UIKit
extension UIAlertController{
private struct AssociatedKeys {
static var alertWindow = "alertWindow"
}
var alertWindow:UIWindow?{
get{
guard let alertWindow = objc_getAssociatedObject(self, &AssociatedKeys.alertWindow) as? UIWindow else {
return nil
}
return alertWindow
}
set(value){
objc_setAssociatedObject(self,&AssociatedKeys.alertWindow,value,objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func show(animated:Bool) {
self.alertWindow = UIWindow(frame: UIScreen.main.bounds)
self.alertWindow?.rootViewController = UIViewController()
self.alertWindow?.windowLevel = UIWindow.Level.alert + 1
if #available(iOS 13, *){
let mySceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
mySceneDelegate!.window?.rootViewController?.present(self, animated: animated, completion: nil)
}
else{
self.alertWindow?.makeKeyAndVisible()
self.alertWindow?.rootViewController?.present(self, animated: animated, completion: nil)
}
}
}
In addition to the answers about creating a reference to UIWindow and then presenting modally, I've included a section of my code on how I'm dismissing it.
class PresentingViewController: UIViewController {
private var coveringWindow: UIWindow?
func presentMovie() {
let playerVC = MoviePlayerViewController()
playerVC.delegate = self
playerVC.modalPresentationStyle = .overFullScreen
playerVC.modalTransitionStyle = .coverVertical
self.coverPortraitWindow(playerVC)
}
func coverPortraitWindow(_ movieController: MoviePlayerViewController) {
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
if let windowScene = windowScene as? UIWindowScene {
self.coveringWindow = UIWindow(windowScene: windowScene)
let rootController = UIViewController()
rootController.view.backgroundColor = .clear
self.coveringWindow!.windowLevel = .alert + 1
self.coveringWindow!.isHidden = false
self.coveringWindow!.rootViewController = rootController
self.coveringWindow!.makeKeyAndVisible()
rootController.present(movieController, animated: true)
}
}
func uncoverPortraitWindow() {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate
else {
return
}
sceneDelegate.window?.makeKeyAndVisible()
self.coveringWindow = nil
}
}
Related
I have a custom alert which is presented as a UIView on a UIViewController. So when I want to present the alert I present the UIViewController which brings up the alert. The problem that I'm having is that I want to do a simple animation on the alert when it's shown. But as it is presented as a UIViewController and not just shown with a UIView overlay, I'm a bit confused as how to accomplish this. Here's the relevant code:
static func standardMessageAlert(title: String, msg: String, action: ((_ text: String?) -> Void)? = nil) -> CustomAlertViewController? {
let alert = CustomAlertViewController.create(title: title, message: msg)
let okAction = CustomAlertAction(title: "OK", type: .normal, handler: action)
alert?.addAction(okAction)
return alert
}
And the code for CustomAlertViewController, which is pretty big, but the create method looks like this:
static func create(title: String?, message: String?, addon: Addon? = nil, alertType: AlertType? = nil) -> CustomAlertViewController? {
let storyboard = UIStoryboard(name: "Overlay", bundle: nil)
guard let viewController = storyboard.instantiateViewController(withIdentifier: "CustomAlertViewController") as? CustomAlertViewController else {
return nil
}
viewController.modalPresentationStyle = .overCurrentContext
viewController.modalTransitionStyle = .crossDissolve
viewController.alertType = alertType
viewController.addon = addon
viewController.alertTitle = title
viewController.alertMessage = message
return viewController
}
...
The alert is presented like this:
func present(animated: Bool, onView: UIViewController? = UIApplication.rootViewController(), completion: (() -> Void)? = nil) {
onView?.present(self, animated: animated, completion: nil)
}
The animation would be something basic like this:
func animateView() {
alertView.alpha = 0
UIView.animate(withDuration: 1, animations: { [weak self] () -> Void in
guard let self = self else { return }
self.alertView.alpha = 1.0
})
}
alertView is the outlet for the alert UIView that is inside the UIViewController. I guess this is what I should animate, but where do I put that code? How do I implement it when the UIViewController is presented? I have also tried using onView.view to animate it. The problem that I see is that if I put the animation code before presenting it's too early, and if it's after presenting then its too late.
If you want to keep this logic then I can suggest you to setup initial view state in viewDidLoad and run animation after viewWillAppear.
But I recommend to use custom transitions
I was trying to follow this tutorial to create a multi screen application:
https://www.youtube.com/watch?v=UYviLiI2rlY&t=774s
Unfortunately at min 25:00 - 26:00 I receive an error and my external screen stay black:
[Assert] Error in UIKit client: -[UIWindow setScreen:] should not be called if the client adopts
UIScene lifecycle. Call -[UIWindow setWindowScene:] instead.
My code is:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
var additionalWindows = [UIWindow]()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, object: nil, queue: nil) { [weak self] notification in
guard let self = self else {return}
guard let newScreen = notification.object as? UIScreen else {return}
let screenDimensions = newScreen.bounds
let newWindow = UIWindow(frame: screenDimensions)
newWindow.screen = newScreen
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: "PreviewViewController") as? PreviewViewController else {
fatalError("Unable to find PreviewViewController")
}
newWindow.rootViewController = vc
newWindow.isHidden = false
self.additionalWindows.append(newWindow)
}
}
}
And I have a deprecation alert in newWindow.screen = newScreen : Setter for 'screen' was deprecated in iOS 13.0 but I can't find anything useful and not overcomplicated on how to solve this issue.
Note that you 'should' instantiate a VC as per the externalWindow.rootViewController
In my case, I used the external display for presenting a custom UIView() so I use an empty UIViewController() and then add my view to it.
private func setupExternalScreen(screen: UIScreen, retryUntilSet: Bool = true, retryTimesUntilDiscarded: Int = 0) {
var matchingWindowScene: UIWindowScene? = nil
let scenes = UIApplication.shared.connectedScenes
for item in scenes {
if let windowScene = item as? UIWindowScene {
if (windowScene.screen == screen) {
matchingWindowScene = windowScene
break
}
}
}
if matchingWindowScene == nil {
if true == retryUntilSet {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if retryTimesUntilDiscarded < 5 {
self.setupExternalScreen(screen:screen, retryUntilSet: false, retryTimesUntilDiscarded += 1)
} else {
let alert = UIAlertController(title: "Not connected", message: "Reconnect the display and try again", preferredStyle: .alert)
let ok = UIAlertAction(title: "Ok", style: .default, handler: nil)
alert.addAction(ok)
self.present(alert, animated: true, completion: nil)
}
}
}
return
}
externalWindow = UIWindow(frame: screen.bounds)
externalWindow.rootViewController = UIViewController()
airplayView.frame = externalWindow.frame
if !externalWindow.subviews.contains(airplayView) {
externalWindow.rootViewController?.view.addSubview(airplayView)
if let _ = view.window {
view.window?.makeKey()
}
} else {
airplayView.updateValues()
}
externalWindow.windowScene = matchingWindowScene
externalWindowWindow.isHidden = false
}
If your app requires iOS<13 you might need to use if #available(iOS 13.0, *) {}
to decide how to setup your external screen.
Forgot to mention... externalWindow is declared inside the ViewController where I demand using the second screen
lazy var externalWindow = UIWindow()
Together with the answer provided here i was able to make it work in the simulator. It seems for iOS 13+ you have to find the scene in UIApplication.shared.connectedScenes.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
var additionalWindows = [UIWindow]()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, object: nil, queue: nil) { [weak self] notification in
guard let self = self else {return}
guard let newScreen = notification.object as? UIScreen else {return}
// Give the system time to update the connected scenes
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Find matching UIWindowScene
let matchingWindowScene = UIApplication.shared.connectedScenes.first {
guard let windowScene = $0 as? UIWindowScene else { return false }
return windowScene.screen == newScreen
} as? UIWindowScene
guard let connectedWindowScene = matchingWindowScene else {
fatalError("Connected scene was not found") // You might want to retry here after some time
}
let screenDimensions = newScreen.bounds
let newWindow = UIWindow(frame: screenDimensions)
newWindow.windowScene = connectedWindowScene
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: "PreviewViewController") as? PreviewViewController else {
fatalError("Unable to find PreviewViewController")
}
newWindow.rootViewController = vc
newWindow.isHidden = false
self.additionalWindows.append(newWindow)
}
}
}
}
I'm not sure about the timing, .now() + 1 seems to work on simulator but I haven't tried on real hardware yet, so you might want to adjust this.
I wondering the best way to present lock screen in iOS(swift).
ex) If the user presses the home button or receives a call, I want to display the lock screen when user re-enter the app.
So, I tried this way.
func applicationWillResignActive(_ application: UIApplication) {
guard let passcodeManageView = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "passcodeManageView") as? PasscodeManageViewController else { return }
if let window = self.window, let rootViewController = window.rootViewController {
var currentController = rootViewController
while let presentController = currentController.presentedViewController {
currentController = presentController
}
currentController.present(passcodeManageView, animated: true, completion: nil)
}
}
Actually it works pretty well.
However, if the alert window is displayed, it does not work normally.
How can I fixed it? (Sorry for my eng.)
Alert views are always an issue in these cases. A quick solution might be to check if alert view is presented and dismiss it. I played with the following:
func showOverlayController(currentController: UIViewController) {
currentController.present(OverlayViewController(), animated: true, completion: nil)
}
if let window = UIApplication.shared.keyWindow, let rootViewController = window.rootViewController {
var currentController = rootViewController
while let presentController = currentController.presentedViewController {
guard presentController as? UIAlertController == nil else {
presentController.dismiss(animated: false) {
showOverlayController(currentController: currentController)
}
return
}
currentController = presentController
}
showOverlayController(currentController: currentController)
}
Putting aside animations and all this still seems very bad because I suspect if if a view controller inside navigation controller or tab bar controller (or any other type of content view controller) would present an alert view this issue would again show itself. You could use the same logic of finding a top controller to always present alert view on top controller to overcome this.
So I moved to another way which is I would rather change the root view controller instead of presenting an overlay. So I tried the following:
static var currentOverlay: (overlay: OverlayViewController, stashedController: UIViewController)?
static func showOverlay() {
guard currentOverlay == nil else { return }
guard let currentController = UIApplication.shared.keyWindow?.rootViewController else { return }
let overlay: (overlay: OverlayViewController, stashedController: UIViewController) = (overlay: OverlayViewController(), stashedController: currentController)
self.currentOverlay = overlay
UIApplication.shared.keyWindow?.rootViewController = overlay.overlay
}
static func hideOverlay() {
guard let currentOverlay = currentOverlay else { return }
self.currentOverlay = nil
UIApplication.shared.keyWindow?.rootViewController = currentOverlay.stashedController
}
It works great... Until alert view is shown again. So after a bit of an inspection I found out that in case of alert views your application has multiple windows. It makes sense an alert would create a new window over the current one but I am unsure how did anyone think it would be intuitive or that it would in any possible way make sense that you are presenting alert view. I would then expect something like UIApplication.shared.showAlert(alert) but let's put this stupidity aside.
The only real solution I see here is to add a new window for your dialog. To do that you could look around the web. What seems to work for me is the following:
static var currentOverlayWindow: (overlay: OverlayViewController, window: UIWindow, previousWindow: UIWindow)?
static func showOverlay() {
guard currentOverlay == nil else { return }
guard let currentWindow = UIApplication.shared.keyWindow else { return }
let overlay: (overlay: OverlayViewController, window: UIWindow, previousWindow: UIWindow) = (overlay: OverlayViewController(), window: UIWindow(frame: currentWindow.bounds), previousWindow: currentWindow)
self.currentOverlayWindow = overlay
overlay.window.backgroundColor = UIColor.black
overlay.window.rootViewController = overlay.overlay
overlay.window.windowLevel = UIWindowLevelAlert + 1
overlay.window.makeKeyAndVisible()
}
static func hideOverlay() {
guard let currentOverlayWindow = currentOverlayWindow else { return }
self.currentOverlay = nil
currentOverlayWindow.window.isHidden = true
currentOverlayWindow.previousWindow.makeKeyAndVisible()
}
Just to fill in the gaps. What I used as an overlay view controller:
class OverlayViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 12.0, y: 100.0, width: 150.0, height: 55.0))
button.setTitle("Dismiss", for: .normal)
view.addSubview(button)
button.addTarget(self, action: #selector(onButton), for: .touchUpInside)
}
#objc private func onButton() {
AppDelegate.hideOverlay()
// self.dismiss(animated: true, completion: nil)
}
}
When APP is Launching - start SigninView - it's Okey. Next if success - I need showTripController(). Function work but nothing show? What's a problem?
func showSigninView() {
let controller = self.window?.rootViewController!.storyboard?.instantiateViewControllerWithIdentifier("DRVAuthorizationViewController")
self.window?.rootViewController!.presentViewController(controller!, animated: true, completion: nil)
}
func showTripController() {
let cv = self.window?.rootViewController!.storyboard?.instantiateViewControllerWithIdentifier("DRVTripTableViewController")
let nc = UINavigationController()
self.window?.rootViewController!.presentViewController(nc, animated:true, completion: nil)
nc.pushViewController(cv!, animated: true);
}
First of all you must add this before you use window :
self.window.makeKeyAndVisible()
Another thing to keep in mind is:
Sometimes keyWindow may have been replaced by window with nil rootViewController (showing UIAlertViews, UIActionSheets on iPhone, etc), in that case you should use UIView's window property.
So, instead of using rootViewController, use the top one presented by it:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
if let topController = UIApplication.topViewController() {
topController.presentViewController(vc, animated: true, completion: nil)
}
Replace last 3 lines of showTripController as below:
let nc = UINavigationController(rootViewController: cv));
self.window!.rootViewController = nc
I'm trying to present a ViewController if there is any saved data in the data model. But I get the following error:
Warning: Attempt to present * on *whose view is not in the window hierarchy"
Relevant code:
override func viewDidLoad() {
super.viewDidLoad()
loginButton.backgroundColor = UIColor.orangeColor()
var request = NSFetchRequest(entityName: "UserData")
request.returnsObjectsAsFaults = false
var appDel:AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext!
var results:NSArray = context.executeFetchRequest(request, error: nil)!
if(results.count <= 0){
print("Inga resultat")
} else {
print("SWITCH VIEW PLOX")
let internVC = self.storyboard?.instantiateViewControllerWithIdentifier("internVC") as internViewController
self.presentViewController(internVC, animated: true, completion: nil)
}
}
I've tried different solutions found using Google without success.
At this point in your code the view controller's view has only been created but not added to any view hierarchy. If you want to present from that view controller as soon as possible you should do it in viewDidAppear to be safest.
In objective c:
This solved my problem when presenting viewcontroller on top of mpmovieplayer
- (UIViewController*) topMostController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
Swift 3
I had this keep coming up as a newbie and found that present loads modal views that can be dismissed but switching to root controller is best if you don't need to show a modal.
I was using this
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard?.instantiateViewController(withIdentifier: "MainAppStoryboard") as! TabbarController
present(vc, animated: false, completion: nil)
Using this instead with my tabController:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let view = storyboard.instantiateViewController(withIdentifier: "MainAppStoryboard") as UIViewController
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//show window
appDelegate.window?.rootViewController = view
Just adjust to a view controller if you need to switch between multiple storyboard screens.
Swift 3.
Call this function to get the topmost view controller, then have that view controller present.
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
Usage:
let topVC = topMostController()
let vcToPresent = self.storyboard!.instantiateViewController(withIdentifier: "YourVCStoryboardID") as! YourViewController
topVC.present(vcToPresent, animated: true, completion: nil)
for SWIFT
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.sharedApplication().keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
You just need to perform a selector with a delay - (0 seconds works).
override func viewDidLoad() {
super.viewDidLoad()
perform(#selector(presentExampleController), with: nil, afterDelay: 0)
}
#objc private func presentExampleController() {
let exampleStoryboard = UIStoryboard(named: "example", bundle: nil)
let exampleVC = storyboard.instantiateViewController(withIdentifier: "ExampleVC") as! ExampleVC
present(exampleVC, animated: true)
}
Swift 4
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
Use of main thread to present and dismiss view controller worked for me.
DispatchQueue.main.async { self.present(viewController, animated: true, completion: nil) }
I was getting this error while was presenting controller after the user opens the deeplink.
I know this isn't the best solution, but if you are in short time frame here is a quick fix - just wrap your code in asyncAfter:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7, execute: { [weak self] in
navigationController.present(signInCoordinator.baseController, animated: animated, completion: completion)
})
It will give time for your presenting controller to call viewDidAppear.
For swift 3.0 and above
public static func getTopViewController() -> UIViewController?{
if var topController = UIApplication.shared.keyWindow?.rootViewController
{
while (topController.presentedViewController != nil)
{
topController = topController.presentedViewController!
}
return topController
}
return nil}
let storyboard = UIStoryboard(name: "test", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "teststoryboard") as UIViewController
UIApplication.shared.keyWindow?.rootViewController?.present(vc, animated: true, completion: nil)
This seemed to work to make sure it's the top most view.
I was getting an error
Warning: Attempt to present myapp.testController: 0x7fdd01703990 on myapp.testController: 0x7fdd01703690 whose view is not in the window hierarchy!
Hope this helps others with swift 3
I have tried so many approches! the only useful thing is:
if var topController = UIApplication.shared.keyWindow?.rootViewController
{
while (topController.presentedViewController != nil)
{
topController = topController.presentedViewController!
}
}
All implementation for topViewController here are not fully supporting cases when you have UINavigationController or UITabBarController, for those two you need a bit different handling:
For UITabBarController and UINavigationController you need a different implementation.
Here is code I'm using to get topMostViewController:
protocol TopUIViewController {
func topUIViewController() -> UIViewController?
}
extension UIWindow : TopUIViewController {
func topUIViewController() -> UIViewController? {
if let rootViewController = self.rootViewController {
return self.recursiveTopUIViewController(from: rootViewController)
}
return nil
}
private func recursiveTopUIViewController(from: UIViewController?) -> UIViewController? {
if let topVC = from?.topUIViewController() { return recursiveTopUIViewController(from: topVC) ?? from }
return from
}
}
extension UIViewController : TopUIViewController {
#objc open func topUIViewController() -> UIViewController? {
return self.presentedViewController
}
}
extension UINavigationController {
override open func topUIViewController() -> UIViewController? {
return self.visibleViewController
}
}
extension UITabBarController {
override open func topUIViewController() -> UIViewController? {
return self.selectedViewController ?? presentedViewController
}
}
The previous answers relate to the situation where the view controller that should present a view 1) has not been added yet to the view hierarchy, or 2) is not the top view controller.
Another possibility is that an alert should be presented while another alert is already presented, and not yet dismissed.
Swift Method, and supply a demo.
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.sharedApplication().keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
func demo() {
let vc = ViewController()
let nav = UINavigationController.init(rootViewController: vc)
topMostController().present(nav, animated: true, completion: nil)
}
Swift 5.1:
let storyboard = UIStoryboard.init(name: "Main", bundle: Bundle.main)
let mainViewController = storyboard.instantiateViewController(withIdentifier: "ID")
let appDeleg = UIApplication.shared.delegate as! AppDelegate
let root = appDeleg.window?.rootViewController as! UINavigationController
root.pushViewController(mainViewController, animated: true)
Rather than finding top view controller, one can use
viewController.modalPresentationStyle = UIModalPresentationStyle.currentContext
Where viewController is the controller which you want to present
This is useful when there are different kinds of views in hierarchy like TabBar, NavBar, though others seems to be correct but more sort of hackish
The other presentation style can be found on apple doc