Setter for 'screen' was deprecated in iOS 13.0 - ios

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.

Related

How to dismiss view controller from SceneDelegate?

I am working on an ios App (Swift 5) and I am trying to show a screen if the app is offline and then dismisses when the user re-connects.
I am expecting the OfflineViewController to appear when the user is offline, and the last screen the user on to appear if they are connected.
What is happening is the OfflineViewController is appearing when I disconnect from the network, however it does not go away when I connect back to the network. I tried adding a button to dismiss and this does not work either.
I've attached my code below, any idea what am I doing wrong?
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let reachability = try! Reachability()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
// Send to homepage if logged in, otherwise login screen.
let accessToken: String? = KeychainWrapper.standard.string(forKey: "accessToken")
// If access token exists, skip login page
if accessToken != nil {
if let windowScene = scene as? UIWindowScene {
self.window = UIWindow(windowScene: windowScene)
let mainStoryboard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = mainStoryboard.instantiateViewController(withIdentifier: "homeTabController") as! TabBarController
self.window!.rootViewController = vc
}
}
reachability.whenUnreachable = { [self] _ in
print("Not reachable (Scene delegate)")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "OfflineViewController") as! OfflineViewController
vc.modalPresentationStyle = .fullScreen
guard let firstScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return
}
guard let firstWindow = firstScene.windows.first else {
return
}
let rootVC = firstWindow.rootViewController
rootVC?.dismiss(animated: true, completion: nil)
rootVC!.present(vc, animated: true, completion: nil)
}
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
}
}
OfflineViewController
import UIKit
class OfflineViewController: UIViewController {
let reachability = try! Reachability()
override func viewDidLoad() {
super.viewDidLoad()
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
}
#IBAction func hitRefresh(_ sender: Any) {
reachability.whenReachable = { reachability in
self.dismiss(animated: true, completion: nil)
}
}
}
I would start be removing all of the reachability code from OfflineViewController. The logic to dismiss belongs with the logic to present.
Then update the reachability code in the scene delegate with a whenReachable block that dismisses the current OfflineViewController.
You should also avoid using UIApplication.shared.connectedScenes in the scene delegate code. You already know the scene. No need to go find it.
The updated whenUnreachable:
reachability.whenUnreachable = { [self] _ in
print("Not reachable (Scene delegate)")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "OfflineViewController") as! OfflineViewController
vc.modalPresentationStyle = .fullScreen
guard let winScene = (scene as? UIWindowScene) else { return }
guard let firstWindow = winScene.windows.first else {
return
}
let rootVC = firstWindow.rootViewController
rootVC?.dismiss(animated: true, completion: nil)
rootVC?.present(vc, animated: true, completion: nil)
}
The added whenReachable:
reachability.whenReachable = { [self] _ in
print("Reachable (Scene delegate)")
guard let winScene = (scene as? UIWindowScene) else { return }
guard let firstWindow = winScene.windows.first else {
return
}
let rootVC = firstWindow.rootViewController
rootVC?.dismiss(animated: true, completion: nil)
}

Swift return to preview UINavigationController

I have VC like a "Something went wrong". This VC i created like a separately VC(without storyboard) and i want to show it where i want. But in the "Something went wrong" View Controller i have a button "refresh". When a user click to this button he must to go back.
When i have some problem with parsing Json or something like this, i call Something went wrong" View Controller like this:
let navController = UINavigationController()
navController.pushViewController(SomethingWentWorngVC(nibName: "SomethingWentWorngView", bundle: nil), animated: false)
window?.rootViewController = navController
window?.makeKeyAndVisible()
also i have extension for getting window
extension UIViewController {
var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
var sceneDelegate: SceneDelegate? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate else { return nil }
return delegate
}
}
extension UIViewController {
var window: UIWindow? {
if #available(iOS 13, *) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate, let window = delegate.window else { return nil }
return window
}
guard let delegate = UIApplication.shared.delegate as? AppDelegate, let window = delegate.window else { return nil }
return window
}
}
in the SomethingWentWorngVC i have button for go to back
#IBAction func refreshAction(_ sender: Any) {
self.navigationController?.popToRootViewController(animated: false)
}
but it doesnt work
in this alternate way you can use this , initially u need to create the common code in appdelegate using tag, then you need to do the addsubview to window for example,
for show
func showWentWrongScreen(){
let getVC = SomethingWentWorngVC
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
}
for remove
func removeWentWrongScreen(){
if let getWindow = self.window, let getWentWrongView = getWindow.viewWithTag(501){
getWentWrongView.removeFromSuperview()
}
}
now you can use where u need

Swift. After addSubview click listeners dont work

I have a testVC. TestVC hasnt storyboard, this viewController has XIB file. I show this VC when i have no internet. And logic for a show this VC like this:
let getVC = NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil)
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
also i have extension for UIViewController
extension UIViewController {
var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
var sceneDelegate: SceneDelegate? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate else { return nil }
return delegate
}
}
extension UIViewController {
var window: UIWindow? {
if #available(iOS 13, *) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate, let window = delegate.window else { return nil }
return window
}
guard let delegate = UIApplication.shared.delegate as? AppDelegate, let window = delegate.window else { return nil }
return window
}
}
all clicks in the TestVC works if i show this View Controller like this:
navController.pushViewController(NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil), animated: true)
but it doesn't suit me. I need to show NoInternetConnectionVC like i described above, that is, like this. When i show NoInternetConnectionVC like below all my listeners stop to work
let getVC = NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil)
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
I tried to add line isUserInteractionEnabled to my code, like this
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.isUserInteractionEnabled = true //added line
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
but it doesnt work
You have just added it as a subview. After adding subview move your controller to parent.
if let root = UIApplication.shared.windows.first?.rootViewController {
root.addChild(getVC)
getVC.didMove(toParent: root)
}

UIWindow not showing over content in iOS 13

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

Swift – self.navigationController becomes nil after transition

I'm experiencing a very strange error in my app, between two views, self.navigationController is becoming nil
I have a few view controllers: MainViewController, SecondViewController PastSessionsViewController, JournalViewController. I use JournalViewController for two purposes, to save a new entry into CoreData or to edit an older one. The details aren't really relevant to this error.
The error occurs when I try to pop JournalViewController off the stack and return to MainViewController but only when JournalViewController is in "edit" mode, not when it's in "save a new entry mode"
Any idea 1) why this is happening and 2) how to correctly address it so I can return to PastSessionsViewController when coming back from JournalViewController in edit mode?
Here's some code to make things concrete.
In AppDelegate.swift (inside of didFinishLaunchingWithOptions):
navController = UINavigationController()
navController!.navigationBarHidden = true
var viewController = MainViewController()
navController!.pushViewController(viewController, animated: false)
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = navController
window?.makeKeyAndVisible()
In MainViewController:
func goToPastSessions(sender: UIButton) {
let pastSessionsVC = PastSessionsViewController()
self.navigationController?.pushViewController(pastSessionsVC, animated: true)
}
func goToWriteJournalEntry(sender: UIButton) {
let journalVC = JournalViewController(label: "Record your thoughts")
self.navigationController?.pushViewController(journalVC, animated: true)
}
In PastSessionsViewController:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let editJournalVC = JournalViewController(label: "Edit your thoughts")
let indexPath = tableView.indexPathForSelectedRow()
let location = indexPath?.row
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! EntryCell
if let objects = pastSessionsDataSource.coreDataReturn {
if let location = location {
editJournalVC.journalEntryCoreDataLocation = location
editJournalVC.editEntry = true
editJournalVC.journalEntryToEdit = objects[location].journalEntry
}
}
self.navigationController?.presentViewController(editJournalVC, animated: true, completion: nil)
}
And finally, in JournalViewController:
func doneJournalEntry(sender: UIButton) {
journalEntryTextArea?.resignFirstResponder()
var entry = journalEntryTextArea?.text
if let entry = entry {
let appDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
let managedObjectContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "Session")
var error: NSError?
// new entry
if journalEntryText == "Record your thoughts" {
let result = managedObjectContext.executeFetchRequest(request, error: &error)
if let objects = result as? [Session] {
if let lastTime = objects.last {
lastTime.journalEntry = entry
}
}
} else {
// existing entry
let sortDescriptor = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sortDescriptor]
let result = managedObjectContext.executeFetchRequest(request, error: &error)
if let objects = result as? [Session] {
var location = journalEntryCoreDataLocation
var object = objects[location!]
object.journalEntry = entry
}
}
if !managedObjectContext.save(&error) {
println("save failed: \(error?.localizedDescription)")
}
}
// in "edit" mode, self.navigationController is `nil` and this fails
// in "record a new entry" mode, it's not nil and works fine
self.navigationController?.popViewControllerAnimated(true)
}
func cancelEntryOrEditAndReturn(sender: UIButton) {
self.journalEntryTextArea?.resignFirstResponder()
// in "edit" mode, self.navigationController is `nil` and this fails
// in "record a new entry" mode, it's not nil and works fine
self.navigationController?.popViewControllerAnimated(true)
}
Thanks for taking a look
You should push editJournalVC instead of presenting if you want it to be in same navigation stack. Because when you present controller its no longer in same navigation stack. Also if you are presenting it, you should dismiss it not pop. So if you want to present controller you should use
self.dismissViewControllerAnimated(true, completion: nil)
instead
self.navigationController?.popViewControllerAnimated(true)

Resources