Xcode not calling application delegate after opting out of SceneDelegate
So, I finally found the answer.
Turns out it's a good old
Instance Method Nearly Matches
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool
That Xcode completely ignored to report, and somehow managed to break in between building for iOS 11 > iOS 13 > iOS 11.
It's also strange because I verified them a couple of days ago with the documentation, both Apples & Firebase, and there was no difference between them.
In the end, it works now, yay.
iOS 13 push-notifications delegate methods are not called
yes building the code base with XCode 11 and iOS 13 seemed to fix this for me.
- built app with Xcode 10 + iOS 12.
- installed on iOS12
- installed on iOS13.0
- send push
- only appearing on iOS12
- Rebuilt the app with XCode 11 and iOS 13.0
- Send push
- appears on iOS 12.4 + iOS 13.0
iOS Universal Link opens app, does not trigger app delegate methods
Turns out this is not a universal links specific problem, but a change in iOS 13's way of triggering app lifecycle events. Instead of coming through UIApplicationDelegate
, they come through UISceneDelegate
.
One confusing thing is that the app delegate methods aren't deprecated so you won't get a warning if you have both app delegate and scene delegate methods in place but only one will be called.
Refer to App delegate methods aren't being called in iOS 13 for a comprehensive answer
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
}
App operation without app delegate methods
In part, the declaration of the UIApplicationDelegate
protocol looks like this:
public protocol UIApplicationDelegate : NSObjectProtocol {
optional public func applicationDidFinishLaunching(_ application: UIApplication)
optional public func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool
optional public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool
optional public func applicationDidBecomeActive(_ application: UIApplication)
optional public func applicationWillResignActive(_ application: UIApplication)
...
Note that the functions are declared as optional. This means that code that conforms to the protocol does not have to implement those functions.
If your app doesn't need to do anything special when it enters the background, then you don't need to implement applicationDidEnterBackground
. If your app did need to do something, then you would implement that function.
Related Topics
How to Edit Uialertaction Title Font Size and Style
iOS 7 - Restrict Landscape Orientation Only in One View Controller
Is This Code Drawing at the Point or Pixel Level? How to Draw Retina Pixels
How to Read Plist Without Using Nsdictionary in Swift
How to Display Remote Document Using Qlpreviewcontroller in Swift
How to Access an Xcassets Directory on the Filesystem
Receive Accelerometer Updates in Background Using Coremotion Framework
How to Make Offline Database for My App
Uitableviewcell Auto Height Based on Amount of Uilabel Text
Exit Application When Click Button - iOS
Swift Optional Chaining Doesn't Work in Closure
iOS Swift: Could Not Cast Value Type '_Nscfnumber' to 'Nsstring'
Converting Int to Float Loses Precision for Large Numbers in Swift
Setting Up Uiscrollview to Swipe Between 3 View Controllers
How to Force Uiviewcontroller Orientation