View Controller Responds to App Delegate Notifications in iOS 12 But Not in iOS 13

View controller responds to app delegate notifications in iOS 12 but not in iOS 13

When supporting scenes under iOS 13, many of the UIApplicationDelegate lifecycle methods are no longer called. There are now corresponding lifecycle methods in the UISceneDelegate. This means there is a need to listen to the UIScene.didEnterBackgroundNotification notification under iOS 13. You can find more details in the documentation at the Managing Your App's Life Cycle page.

You need to update the notification observer code to:

if #available(iOS 13.0, *) {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIScene.didEnterBackgroundNotification, object: nil)
} else {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
}

This allows your view controller (or view) to listen to the correct event depending on which version of iOS it is running under.

The same didEnterBackground method is called for both events depending on the version of iOS.


But there is an added complication if your app supports multiple windows.

If the user of your app has opened multiple windows of your app, then every copy of this view controller (or view) will be notified of the background event even if the given view controller is still in the foreground or if has been in the background all along.

In the likely case you only want the one window that was just put into the background to respond to the event, you need to add an extra check. The object property of the notification will tell you which specific scene has just entered the background. So the code needs to check to see if the notification's window scene is scene associated with the view controller (or view).

Brief side trip: See this answer for details on how to get the UIScene of a UIViewController or UIView. (It's not as straightforward as you would hope).

This requires an update to the didEnterBackground method as follows:

@objc func didEnterBackground(_ notification: NSNotification) {
if #available(iOS 13.0, *) {
// This requires the extension found at: https://stackoverflow.com/a/56589151/1226963
if let winScene = notification.object as? UIWindowScene, winScene === self.scene {
return; // not my scene man, I'm outta here
} // else this is my scene, handle it
} // else iOS 12 and we need to handle the app going to the background

// Do my background stuff
}

There is a way to make this a little simpler. When registering with NotificationCenter, you can specify your own window scene as an argument to the object parameter. Then the didEnterBackground method will only be called for your own window scene.

The trick with this is getting your own window scene at the time you register for the notification. Since you can only get a view controller's scene after viewDidAppear has been called at least once, you can't use any init, viewDidLoad, or even viewWillAppear. Those are all too early.

Since viewDidAppear can be called more than once, you will end up calling addObserver each time and that is a problem because then your handler will get called multiple times for a single event. So one thought is to unregister the observer in viewDidDisappear. But then this now has the problem of your view controller not being called if some other view controller is covering it. So the trick it to add the observer in viewDidAppear but only the first time it is called for a specific instance of the view controller.

If you can wait until viewDidAppear, then first you need to add a property to your class to keep track of whether it's been viewed yet or not.

var beenViewed = false

Then add viewDidAppear:

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

if !beenViewed {
beenViewed = true

if #available(iOS 13.0, *) {
// Only be notified of my own window scene
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIScene.didEnterBackgroundNotification, object: self.view.window?.windowScene)
} else {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}
}

And then your didEnterBackground can be the old simple version again:

@objc func didEnterBackground() {
// Do my background stuff
}

For Objective-C, the code is as follows:

Register for the notifications before viewDidAppear:

if (@available(iOS 13.0, *)) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UISceneDidEnterBackgroundNotification object:nil];
} else {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
}

The more complicated didEnterBackground:

- (void)didEnterBackground:(NSNotification *)notification {
if (@available(iOS 13.0, *)) {
// This requires the extension found at: https://stackoverflow.com/a/56589151/1226963
if (notification.object != self.scene) {
return; // not my scene
} // else my own scene
} // else iOS 12

// Do stuff
}

If you want to use viewDidAppear and have a simpler didEnterBackground:

Add an instance variable to your class:

BOOL beenViewed;

Then add viewDidAppear:

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

if (!beenViewed) {
beenViewed = YES;

if (@available(iOS 13.0, *)) {
// Only be notified of my own window scene
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground) name:UISceneDidEnterBackgroundNotification object:self.view.window.windowScene];
} else {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
}
}
}

And the simpler didEnterBackground:

- (void)didEnterBackground {
// Do stuff
}

AppDelegate and SceneDelegate when supporting iOS 12 and 13

You do need to duplicate the code but you need to make sure it runs only on the correct system. In iOS 13 you don’t want that application delegate didFinishLaunching body code to run, so use an availability check to prevent it.
In the same way, use availability to hide the window scene stuff from iOS 12.

Here's the basic sketch of a solution that runs correctly on both iOS 12 and iOS 13:

AppDelegate.Swift

import UIKit
@UIApplicationMain
class AppDelegate : UIResponder, UIApplicationDelegate {
var window : UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey : Any]?)
-> Bool {
if #available(iOS 13, *) {
// do only pure app launch stuff, not interface stuff
} else {
self.window = UIWindow()
let vc = ViewController()
self.window!.rootViewController = vc
self.window!.makeKeyAndVisible()
self.window!.backgroundColor = .red
}
return true
}
}

SceneDelegate.swift

import UIKit
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window : UIWindow?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
self.window = UIWindow(windowScene: windowScene)
let vc = ViewController()
self.window!.rootViewController = vc
self.window!.makeKeyAndVisible()
self.window!.backgroundColor = .red
}
}
}

ViewController.swift

import UIKit
class ViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("view did load")
self.view.backgroundColor = .green
}
}

Note that dealing with other duplicates, such as the application activating, is much simpler because if you support window scenes the application delegate method won't be called on iOS 12. So the problem is confined to this one situation, namely where you have window / root view controller manipulations to perform at launch (e.g. no storyboard).

Black screen after adding SceneDelegate and updating Info.plist

You have several issues here. It's important to read the documentation related to the app lifecycle which states what is called under iOS 13 and what is called under iOS 12.

You may also want to see my Single View App template that supports iOS 12 and 13.

Looking at your code, here is a summary of the problems:

AppDelegate:

  • You should only setup the main window and the root view controller if the app is being run under iOS 12 or earlier. You need to check this at runtime.
  • The func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) method should not be in the app delegate.
  • Not directly related but never sleep on app startup. Remove the Thread.sleep(forTimeInterval: 3.0) line. Users want to use your app, not stare at the launch screen longer than necessary. And blocking the main thread on app launch can cause your app to be killed.

SceneDelegate:

  • This is mostly fine but there is no reason for the guard let _ = (scene as? UIWindowScene) else { return } line, especially since it is inside an if let that already does that check.
  • You don't appear to be using SwiftUI so remove that import.

I would update your app delegate to be more like this:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
UINavigationBar.appearance().barTintColor = UIColor(red:0.08, green:0.23, blue:0.62, alpha:1.0)

if #available(iOS 13.0, *) {
// In iOS 13 setup is done in SceneDelegate
} else {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window

if (user != nil && userSelfIdent != nil){
let mainstoryboard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let newViewcontroller:UIViewController = mainstoryboard.instantiateViewController(withIdentifier: "swrevealviewcontroller") as! SWRevealViewController
window.rootViewController = newViewcontroller
}
}

return true
}

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13.0, *) {
// In iOS 13 setup is done in SceneDelegate
} else {
self.window?.makeKeyAndVisible()
}

return true
}

func applicationWillResignActive(_ application: UIApplication) {
// Not called under iOS 13 - See SceneDelegate sceneWillResignActive
}

func applicationDidEnterBackground(_ application: UIApplication) {
// Not called under iOS 13 - See SceneDelegate sceneDidEnterBackground
}

func applicationWillEnterForeground(_ application: UIApplication) {
// Not called under iOS 13 - See SceneDelegate sceneWillEnterForeground
}

func applicationDidBecomeActive(_ application: UIApplication) {
// Not called under iOS 13 - See SceneDelegate sceneDidBecomeActive
}

// MARK: UISceneSession Lifecycle

@available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

@available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}

Your scene delegate could be like:

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }

let window = UIWindow(windowScene: windowScene)
self.window = window

if (user != nil && userSelfIdent != nil){
let mainstoryboard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let newViewcontroller:UIViewController = mainstoryboard.instantiateViewController(withIdentifier: "swrevealviewcontroller") as! SWRevealViewController
window.rootViewController = newViewcontroller
window.makeKeyAndVisible()
}
}

func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
}

func sceneDidBecomeActive(_ scene: UIScene) {
// Not called under iOS 12 - See AppDelegate applicationDidBecomeActive
}

func sceneWillResignActive(_ scene: UIScene) {
// Not called under iOS 12 - See AppDelegate applicationWillResignActive
}

func sceneWillEnterForeground(_ scene: UIScene) {
// Not called under iOS 12 - See AppDelegate applicationWillEnterForeground
}

func sceneDidEnterBackground(_ scene: UIScene) {
// Not called under iOS 12 - See AppDelegate applicationDidEnterBackground
}
}

View will appear is not firing when we dismiss notification settings overlay

You should be using app lifecycle events (SceneDelegate/AppDelegate), not view controller lifecycle events (viewDidLoad, viewDidAppear, etc). sceneDidBecomeActive(_:) should be fine for your purposes — for iOS 13+, you should be using SceneDelegate to listen to scene phases, like going to settings (becoming inactive) and then coming back (becoming active again).

/// SceneDelegate.swift
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.

/// your code here
}

If you want to listen to sceneDidBecomeActive directly in your view controller, try listening to the didActivateNotification notification.

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

NotificationCenter.default.addObserver( /// add observer
self,
selector: #selector(activated),
name: UIScene.didActivateNotification,
object: nil
)
}

@objc func activated() {
print("View controller is back now")
}
}

Switching ViewControllers when tapping a notification in Swift 5

So the problem is how to talk to the scene delegate?

The application is UIApplication.shared. The application has connectedScenes. Take the first one and ask for its delegate. Cast that down to SceneDelegate and talk to it.

Or if you want to manipulate the window directly (I don’t advise it), ask the application for its windows. Again, take the first one.

UI state restoration for a scene in iOS 13 while still supporting iOS 12. No storyboards

To support state restoration in iOS 13 you will need to encode enough state into the NSUserActivity:

Use this method to return an NSUserActivity object with information about your scene's data. Save enough information to be able to retrieve that data again after UIKit disconnects and then reconnects the scene. User activity objects are meant for recording what the user was doing, so you don't need to save the state of your scene's UI

The advantage of this approach is that it can make it easier to support handoff, since you are creating the code necessary to persist and restore state via user activities.

Unlike the previous state restoration approach where iOS recreated the view controller hierarchy for you, you are responsible for creating the view hierarchy for your scene in the scene delegate.

If you have multiple active scenes then your delegate will be called multiple times to save the state and multiple times to restore state; Nothing special is needed.

The changes I made to your code are:

AppDelegate.swift

Disable "legacy" state restoration on iOS 13 & later:

func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
if #available(iOS 13, *) {

} else {
print("AppDelegate viewControllerWithRestorationIdentifierPath")

// If this is for the nav controller, restore it and set it as the window's root
if identifierComponents.first == "RootNC" {
let nc = UINavigationController()
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc

return nc
}
}
return nil
}

func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
print("AppDelegate willEncodeRestorableStateWith")
if #available(iOS 13, *) {

} else {
// Trigger saving of the root view controller
coder.encode(self.window?.rootViewController, forKey: "root")
}
}

func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
print("AppDelegate didDecodeRestorableStateWith")
}

func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldSaveApplicationState")
if #available(iOS 13, *) {
return false
} else {
return true
}
}

func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldRestoreApplicationState")
if #available(iOS 13, *) {
return false
} else {
return true
}
}

SceneDelegate.swift

Create a user activity when required and use it to recreate the view controller. Note that you are responsible for creating the view hierarchy in both normal and restore cases.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
print("SceneDelegate willConnectTo")

guard let winScene = (scene as? UIWindowScene) else { return }

// Got some of this from WWDC2109 video 258
window = UIWindow(windowScene: winScene)

let vc = ViewController()

if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
vc.continueFrom(activity: activity)
}

let nc = UINavigationController(rootViewController: vc)
nc.restorationIdentifier = "RootNC"

self.window?.rootViewController = nc
window?.makeKeyAndVisible()

}

func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
print("SceneDelegate stateRestorationActivity")

if let nc = self.window?.rootViewController as? UINavigationController, let vc = nc.viewControllers.first as? ViewController {
return vc.continuationActivity
} else {
return nil
}

}

ViewController.swift

Add support for saving and loading from an NSUserActivity.

var continuationActivity: NSUserActivity {
let activity = NSUserActivity(activityType: "restoration")
activity.persistentIdentifier = UUID().uuidString
activity.addUserInfoEntries(from: ["Count":self.count])
return activity
}

func continueFrom(activity: NSUserActivity) {
let count = activity.userInfo?["Count"] as? Int ?? 0
self.count = count
}

Local notifications didReceive in iOS 13

There are several ways one simple way is to do post notification through NotficationCeneter.

NotificationCenter.default.post(name: Notification.Name("SOME_NAME"), object: nil, userInfo: nil)

And you should add an observer for this notification.

 NotificationCenter.default.addObserver(self, selector: #selector(someMethod(notification: )), name: Notification.Name("SOME_NAME"), object: nil)

There are also different options

For example, I create a simple Navigator class which is Singleton and who keeps the top NavigationController and all the knowledge about the current stack of views.
And if I need to open some specific screen from the AppDelegate, I'm using something like:

MyNavigator.shared.goToSomeScreen()

Still, everything depends on what is your current code and what is your need, for sure you can find something that fits better for you.



Related Topics



Leave a reply



Submit