What can make a UIViewController temporarily become unresponsive? - ios

My app has four View Controllers (VC's):
1) Home: Has a SpriteKit animation on an SKView. A swipe on this VC lets the user proceed to the Authoring VC (next).
2) Authoring: Has a menu (a UITableView). The menu lets the user access ViewGames and Store (below).
3) ViewGames: Contains a UICollectionView, and a nav bar with a close button. This presents a single UICollectionViewCell at a time, and lets the user swipe to proceed to the next cell. Each cell has a SpriteKit animation on a SKView, and also has three buttons.
4) Store: Has an in-app purchase store, with UI implemented as a UITableView. For the purpose of this discussion, the only feature I'm using is SKProductsRequest to fetch back a list of products which are displayed in the table view.
Problem: In the ViewGames VC, under some circumstances, the UI of the second and following cells in the collection view operate very slowly. E.g., about a factor of 10 slower than normal. The animation in the SKView is very slow. And the four buttons (three on the collection view cell, one in the nav bar) operate very slowly. Often they won't respond at all and you have to tap them several times. The swipe to go to the next cell responds similarly-- and slowly, if at all. (If I go back to the first cell using swiping, the first cell is also similarly responsive, but initially it doesn't exhibit this problem).
Reproducing the problem. The good news is, that in my app, reproducing this problem is very consistent. Here's what will produce it:
Launch app > Swipe to go to Authoring > Use menu to go to the Store >
exit Store to return to Authoring > exit Authoring back to Home > go
to Authoring > go to View Games.
Some other facets:
A) If I exit View Games, back to Authoring, and then re-enter View Games, this problem is the same.
B) No other parts of the app exhibit this sluggishness of UI response.
C) If, after getting this behavior in the View Games VC, I now exit ViewGames back to Authoring, re-enter the Store, go back to Authoring, and go back to ViewGames, the problem goes away.
D) This problem is only exhibited on iOS9, iOS9.1, iOS9.2 (beta). It doesn't occur on iOS8.4. (All running on physical devices; I've yet to try the simulator). I was using Xcode 7.0.1 initially, but am now using Xcode 7.2 beta and the problem remains the same. My app is targeted at iOS8 and above.
E) If I launch the app, and go to Authoring, then ViewGames, this problem doesn't occur.
Question: What can make part of the UI run sluggishly, but only temporarily?
Avenues explored so far:
(i) I've looked at this app in the Time Profiling Instrument, but can't see anything that looks like its soaking up time.
(ii) Only one part of the app is doing network interaction, and that's the Store. And the product fetch succeeds, and displays that info.
(iii) My best guess right now is that this is related to memory usage. When the symptoms appear, it appear that there is at least a somewhat greater amount of RAM used in going from cell 1 of the Authoring UICollectionView to cell 2 (0.4 to 0.9MB in cases where the problem appears; 0.3MB in cases where the problem does not appear).
(iv) In the development history of the app, as I was getting ready to submit v1.0 to Apple, I had a memory leak that exhibited some of these symptoms. However, to my recollection, that memory leak only affected the SpriteKit animations, affected all SpriteKit animations (both on the Home and Authoring VC's), and was not temporary. You had to restart the app to get around it.
(v) I've looked at the app quite a bit using Instruments/Leaks/Allocations. There are some leaks, but they appear to be from Apple frameworks, not mine.
(vi) I've put breakpoints and log messages in the dealloc/deinit methods and all of the primary classes seem to be deallocating (e.g., the VC's, and the collection view and it's cells).
Update1: 11/4/15; 3:47pm MST: The problem is not related specifically to the ViewGames SpriteKit animation. I just disabled the animation in the ViewGames UICollectionViewCell's and the problem still occurs. The sluggishness still happens for swipe and button press responses. Of course, the cells still have an SKView/SKScene.
Update2: 11/4/15; 3:55pm MST: I just disabled the product fetch (which uses SKProductsFetch) out of the Store. AND the problem goes away!! Significant narrowing down of the issue!
Update3: 11/4/15; 6:10pm MST: With the product fetch in place, but with the delegate of the SKProductsFetch object set to nil, the problem does not occur! It is also relevant to note that a completion handler (called fetchProductsCompletion) that was part of my class construction was also set to nil.
Update4: 11/4/15; 6:10pm MST: With the product fetch in place, and with a non-nil delegate for SKProductsFetch, but with fetchProductsCompletion set to nil, the problem does not occur!

You could use sleep(amountTime) if you want to freeze the app totatly.
Or use runAfterDelay(amountTime){} to make a delay.

I've found a work-around for the problem. I don't know why it works. But, I'll post some of the code to give a context.
This is the method I am using in the Store to fetch products:
// Not calling this from init, so that we can make sure there is a network connection, and only give a single error/alert if there is no network connection.
private func initiateProductFetch(done:(success:Bool)->()) {
let productIds = self.productsInfo!.productIds()
if nil == productIds {
Assert.badMojo(alwaysPrintThisString: "No product Ids!")
}
/* Bug #C34, #C39.
10/30/15; I was having a memory retention issue in regards to the fetchProducts completion handler. The SMIAPStore instance wasn't getting deallocated until the *next* time the store was presented because the store delegate was retaining a reference to the completion handler, which had a strong reference to self (SMIAPStore).
At first I tried to resolve this by having [unowned self] in the following, but that fails with an exception. Not sure why. And having [weak self] also causes self! to fail-- "fatal error: unexpectedly found nil while unwrapping an Optional value". Why?
The only way I've found to work around this is to have a selfCopy as below, which is nil'ed out in the completion handler. Seems really clumsy. For consistency sake, and safety sake, I'm also using this technique in the other self.storeDelegate usages in this class.
*/
var selfCopy:SMIAPStore? = self
self.storeDelegate?.fetchProducts(productIds!) {
(products:[SKProduct]?, error:NSError?) in
Log.msg("\(products)")
if (products != nil && products!.count > 0 && nil == error) {
// No error.
selfCopy!.productsInfo!.products = products
Log.msg("Done fetching products")
done(success:true)
}
else {
// Show an error. The VC will not be displayed by now. Returning the error with the done call back will not allow it to be displayed.
// It seems possible the products are empty and error is nil (at least this occurs in my debug case above).
var message = ""
if error != nil {
message = error!.localizedDescription
}
let alert = UIAlertView(title: "Couldn't get product information from Apple!", message: message, delegate: nil, cancelButtonTitle: SMUIMessages.session().OkMsg())
selfCopy!.userMessageDetails = UserMessage.session().showAlert(alert, ofType: UserMessageType.Error) { buttonNumber in
done(success:false)
}
}
selfCopy = nil
}
}
The store delegate method (my construction) looks like this:
// Call this first to initiate the fetch of the SKProduct info from Apple
public func fetchProducts(productIdentifiers:[String], done: (products:[SKProduct]?, error:NSError?) ->()) {
self.requestDelegateUseType = .ProductsRequest
self.productsRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
self.productsRequest!.delegate = self
self.fetchProductsCompletion = done
self.productsRequest!.start()
}
And here is the delegate method for the SKProductsRequest:
// Seems like this delegate method gets called *before* the requestDidFinish method. Don't really want to rely on that behavior though.
public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
Assert.If(.ProductsRequest != self.requestDelegateUseType, thenPrintThisString: "Didn't have a product request")
if 0 == response.invalidProductIdentifiers.count {
/*
#if DEBUG
// Debugging-- simulate an error fetching products.
self.fetchProductsCompletion!(products: nil, error: nil)
return
#endif
*/
self.fetchProductsCompletion?(products: response.products, error: nil)
//self.fetchProductsCompletion = nil
}
else {
let message = "Some products were invalid: \(response.invalidProductIdentifiers)"
self.fetchProductsCompletion!(products: nil, error: NSError.create(message))
}
}
The code as given above does exhibit the problem. When I set the fetchProductsCompletion handler to nil after it is used, the problem goes away. Any ideas as to why?

Related

AdMob Refresh Requests

I have several questions that I'm confused with:
Who is best responsible for ad refreshing? After I load the request bannerView.load(GADRequest()) is it best practice to let the developer or Google monitor it?
Should the refresh times be as little as 30 seconds or a minimum of 60 seconds? It seems odd that they recommend 60 seconds but give you the option to choose 30 seconds. I've read on different threads that you can get penalized for all sorts of things and your AdMob account will get closed no questions asked. If that's the case if you pick 30 seconds inside the console and they recommend 60, if they feel anything is "funny" they can terminate your account based on that alone. It's a very oblique process.
It says If your app is automatically refreshing ads, make sure ad requests are not made when the screen is off. If the answer to my first question is to let Google monitor/automatically refresh (without using my code below) how would Google know whether the screen is off or not? For example, the ad is shown in viewControllerA but the user presses a button and viewControllerB is pushed on. Since viewControllerA is still on the stack the ad will constantly get refreshed, I don't see how Google would know the screen is off until viewControllerA is deallocated. Also, if the user went to the background how would Google know? I'm assuming off means the user can no longer see the screen which means they can't see the ad so there's no need to refresh because they either switched tabs, pushed on another VC, or went to the background (all cases handled in the below code)
Where is the link to the refresh console page? Before you could go to https://apps.admob.com > Monetize and you would end up on the page with the refresh options. When I log in I no longer see a Monetize option but get a sidebar with a bunch of icons. None of the icons bring me to the refresh page. FYI I just started using AdMob yesterday and I only saw the refresh page in videos and online tutorials so I never used it before.
It says here (emphasis mine):
Refreshing ads
We recommend that you have ads persist for 60 seconds
or longer, depending on the functionality of your app. Our internal
tests have shown that this ensures users have enough time to engage
with ads, providing the best performance for both advertisers and
publishers. Furthermore, these tests have shown that refreshing ads
more often can hurt fill rate for our publishers.
If your app is automatically refreshing ads, make sure ad requests are
not made when the screen is off. Also, if users navigate to and from
pages with ads in an app over a short period of time, a new ad request
should not be made sooner than the recommended 60 second rate.
But it also says here:
Banner ads
We recommend using the Google-optimized automatic refresh
rate. The optimized rate is calculated using AdMob historical data to
ensure the ads shown in your ad units are being refreshed at the best
rate for banner ads.
You may also set a custom refresh rate of 30-120 seconds or disable
refresh rate completely.
Here's how I would manage the requests myself every 30 secs taking into account when the user goes to the background, switches tabs, or pushes on another VC. If I don't do all of this and left it up to them ("We recommend using the Google-optimized automatic refresh rate"), how would Google know the screen is off or not?
var timer: Timer?
override viewDidLoad() {
super.viewDidLoad()
// instantiate bannerView ...
NotificationCenter.default.addObserver(self, selector: #selector(startAdRefreshTimer), name: UIApplication.willEnterForegroundNotification, object: nil)
// *** the screen is OFF
NotificationCenter.default.addObserver(self, selector: #selector(stopAdRefreshTimer), name: UIApplication.willResignActiveNotification, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
startAdRefreshTimer()
}
// *** the screen is OFF
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopAdRefreshTimer()
}
#objc func startAdRefreshTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 30,
repeats: true,
block: { [weak self](timer) in
self?.refreshAd()
})
}
func refreshAd() {
bannerView.load(GADRequest())
}
#objc func stopAdRefreshTimer() {
if timer != nil {
timer?.invalidate()
timer = nil
}
}
// this really isn't necessary because of what's in viewWillDisappear but I added it anyway
#objc func buttonToPushNextVCTapped() {
let nextVC = NextVC()
navigationController?.pushViewController(viewController: nextVC, animated: true, completion: { [weak self] in
// *** the screen is OFF
self?.stopAdRefreshTimer()
})
}
This isn't the perfect solution because if the user switched tabs back and forth quickly and repeatedly the requests would happen much sooner then 30 seconds which would probably get your account banned. This is just for example purposes.
Answer to my 1st, 2nd, and 3rd question:
1st this is my setup, I have the bannerView inside a headerView which seems to be totally fine. I'm also using a TabBarController. Btw the vc the collectionView is in handles the bannerView and load(GADRequest()). The headerView only displays the ad so I don't have to worry about it constantly getting called as the user scrolls and the headerView is recycled. You can read the link in my set up for more details.
2nd AdMob's BannerView has a delegate method that gets called when a new ad is received:
MyController: GADBannerViewDelegate {
/// Tells the delegate an ad request loaded an ad.
func adViewDidReceiveAd(_ bannerView: GADBannerView) {
print("adViewDidReceiveAd")
}
}
3rd. I placed a break point on that print statement to see what happens and here are the results.
When I left the headerView visible (didn't scroll it off of the scene) the delegate method fired every 60 secs. I could be off by a few seconds because I looked at the time on my computer and didn't use a timer. Anyway pretty much every 60 secs the break point was hit and the test ad was changed.
As soon as I scrolled the headerView off of the scene and waited the breakpoint never got hit which means that the delegate method was never called. I waited 10 minutes and nothing happened -no ads were served.
As soon as I scrolled the headerView back on scene, I waited a several seconds and the breakpoint was eventually hit which means the delegate was called. Like in the first scenario it got hit every 60 seconds after that.
I switched to a different tab, left for a few minutes and the same exact thing in #2 happened, break point wasn't hit because the delegate wasn't called.
As soon as I came back to the tab with the headerView, the same thing in #3 happened, breakpoint was hit every 60secs because the delegate was called.
I went to the background and like #2 again the break point wasn't hit because the delegate wasn't called.
As soon as I came back from the background like #3 the breakpoint was hit every 60secs because the delegate was called.
My conclusion is that the bannerView DEFINITELY knows when it's not present on the scene. Wether a tab is changed, the app goes to the background (probably uses a notification), and most mysteriously it even knows when it's a subView of another view and that view is scrolled on/off the scene.
I'm in the Bronx, NY, it's 8am (I started testing around 7am) and the ad changed basically every 60 seconds. Maybe in other parts of the world, different times of the day, or different times of the year the duration might be longer or shorter?
Knowing what I know now I think that it's best that I let Google monitor the load(GADRequest()) and change the ads when it's ready. An ad changing every 60s is fine by me. If the scene can hold the user's attention for longer than a minute that's 2 ads served. It's more beneficial because I can spend more time trying to keep their attention than focus on serving them an ad.
As far as the code from my question to handle all the off cases it seems the bannerView is capable of handling all of that on it's own. It's less code for me to maintain and deal with and more importantly it's 1 less reason to worry about getting banned for handling something improperly.
In summary (this works best for me)
let Google handle/monitor the bannerView.load(GADRequest()) to serve news ads
don't customize the refresh, it seems to change every 60 secs on it's own
when using a bannerView (and possibly letting Google monitor the load(GADRequest())) ads aren't requested when the screen is off so no need to waste time worrying about it
Answer to my 4th question:
The Monetization page to open App Refresh doesn't seem to exist anymore. After you login into AdMob follow these 5 steps (a new screen will appear for steps 3, 4, and 5 after your click your blue ad unit name in step 2).
I think you will find many confusing things in AdMob banners. To determine what was going on with network calls for ads, I had to use Charles proxy to monitor the network stream. I could not find any evidence that the banner, on any view that wasn't on the top of the stack, was continuing to request ads. But, I didn't dig into the framework to find out how/why because I was trying to solve another issue. I would recommend just allowing Admob to serve the ads with their timing using their stock code stubs:
bannerView = GADBannerView(adSize: kGADAdSizeBanner)
addBannerViewToView(bannerView)
I also added some debug code to the delegate function adViewDidReceiveAd(_ bannerView: GADBannerView) to make sure my app was not serving ads when the view was not on top of the stack. Couldn't find any evidence.
Last, would highly recommend never testing with production ads and to follow their best practices relentlessly on ad placement in your VCs as to not touch or otherwise be directly adjacent to other actionable objects. Very possible you could be put in AdMob jail and not know it for weeks or longer (only that ads will stop serving and you will get the dreaded code 1 "No Ad to Show" debug message).

How to display lock sreen in task switcher and resume correctly

for the app I'm developing, I implemented a lock screen that allows the user to unlock the App by manual-pin or touch/Face-ID.
Everything is working OK during the normal use.
However, I need to show the lock screen when the app is resumed from background and even in the task switcher to avoid "peeking" at the content without having unlocked properly.
As recommended by Apple in this (old) article, I present the lock view controller in applicationDidEnterBackground:
func applicationDidEnterBackground(_ application: UIApplication) {
let lockVC = LoginViewController()
lockVC.loginType = LoginViewController.LoginType.resumeApp
if let topViewController = UIApplication.topViewController() {
topViewController.present(lockVC, animated: false, completion: nil)
}
}
where topViewControler is a useful extension to determnine the topmost view controller: Get top most UIViewController
.
The lockVC.loginType = ... is just to let the ViewController what type of login I need and customize its view a little bit
The results I get are a bit weird, both on simulator and real devices:
the task switcher preview for my app is completely black
when the app is resumed, the screen remains black as in preview and unresponsive. Only way to exit is to kill the app.
before obtaining the weird results above, I had to access all outlets as optional to avoid termination... that's fine for viewDidLoad stuff (I didn't expect the need for this when entering background, since the view had been loaded before - outlet wired) but the strange thing is that I had the same error for an IBAction in viewDidAppear (IBAction for touch-id button called automatically if touch/face-id is available - just a requirement).
I believe I'm missing something big here... but no other hints found.
No one of the ready-made lock screen solutions come with an example for the background/taskSwicth/resume case).
Note that the black/unresponsive screen seems to be same both if I use the mentioned extension for the topmost view controller to present or if I simply try to present it by
self.window?.rootViewController?.present(lockVC, animated: false)
(which I believe is wrong, but tried anyway)
Any help would be apreciated
I found a temporary workaround:
Display the lock screen when the app becomes active from background, as suggested by Dev_Tandel
Hide information in the task switcher by adding a blur effect on the current screen where the app is sent to background (applicationWillResignActive) and removing it when the app comes active again (applicationDidBecomeActive), as suggested here
As said, it's a temporary workaround that i wanted to share, but I don't like this 100%. It is required to show the lock screen in the task swtcher and Ive seen apps doing it (e.g. "oneSafe").
Still looking for help.
Solved, thanks to this post.
Apparently I was naive in trying to present a view controller with just its object instance.
If I use
let lockVC = topViewController.storyboard?.instantiateViewController(withIdentifier: "IDLoginViewController") as! LoginViewController
everything works, the lock screen is shown also in the task switcher, no need to blur!

Load entire application at start up

I have a tabbed iOS application with each of the tabs having some sort of json request so the view loads ever so slightly slowly. I want to remove that lag completely so I'm wondering if there is a way to call the ViewDidLoad function from each of the classes during the login phase of the application.
if (login == "Success") {
UserDefaults.standard.set(true, forKey: "isUserLoggedIn");
UserDefaults.standard.synchronize();
DispatchQueue.main.async {
// Load all resources here
}
}
I can understand this can be bad practice if the app is very large, but I think in this scenario the app isn't huge, but the lag between the view controllers is enough to be annoying. I would rather have the user wait at the start for 3-5 seconds whilst everything loads, and have a smooth experience once inside. Is there a way to do this?
You shouldn't call the lifecycle functions of the viewcontroller for yourself. The viewDidLoad function will be called when the view has been loaded.
Apple: This method is called after the view controller has loaded its view hierarchy into memory.
So calling let _ = viewController.view will trigger the view creation and the call of this function.
But i think it's much better to have a startup phase instead. Think about a 'startup'-screen that downloads everything you need (with maybe a spinner and a text) and moves automatically to the content (the tabbar controller) when done. That may also fix the problem of a low network connectivity (think about a download that take a minute for example). You may display that screen modally above or as screen before the tabbbar controller.
If you don't like the idea of a startup phase you may also design your ui responsive. Start the download whenever needed / regularly and update your ui according to the results when ready. The ui will be fast then, but uses last known data. The meaningfulness and implementation depends on your content.
Update
On second thought: If you already have a server login screen, why not download the content directly after the successful download as part of the login? Users do not know if you are still checking the login or downloading some necessary data. You may say that login is only successful if server login AND download are finished successfully.

"UI Testing Failure: Did not receive view did disappear notification within 2.0s" error

I am trying to record a UI test case and run it back, but I am running into this error. Basically, in my application when I land on the home screen, the user is asked to allow use of notification and location. After I record these events and try to segue to another VC, it records normally like so.
[app.alerts[#"\u201cSampleApp\u201d Would Like to Send You Notifications"].collectionViews.buttons[#"Don\u2019t Allow"] tap];
[app.alerts[#"Allow \u201cSampleApp\u201d to access your location while you use the app?"].collectionViews.buttons[#"Allow"] tap];
//segue to VC2
But when I play it back, it fails with the error in the title.
Did not receive view did disappear notification within 2.0s
I suspect that by the time the alerts are cleared, the segue button is already tapped and while it expects the home VC to disappear, it does not. Is this understanding right?. If yes, how can I delay the expectation, if no, please help.
System level alerts should be handled by addUIInterruptionMonitorWithDescription API here is the documentation from apple Link and sample code in swift below:
addUIInterruptionMonitorWithDescription("First Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
XCUIApplication().tap()

How to use AVCaptureSession with Slide Over and Split View in iOS 9?

My team is developing a set of SDKs for barcode scanning, ID scanning and OCR. We use device's camera, specifically, AVCaptureSession, to obtain video frames on which we perform our processing.
We're exploring new iOS 9 multitasking features Slide Over and Split View.
Apple suggests opting out of these features for camera-centric apps, where using the entire screen for preview and capturing a moment quickly is a primary feature (reference). This is the approach the use in their sample app AVCam.
However, our customers might have apps which don't fall into this category (e.g Mobile banking apps), so we cannot force them to opt out, instead, we need to handle new features in the SDK. We're exploring what would be the best approach to do that, since the docs at the moment aren't really telling us what to do.
We used our simple Camera sample app to analyse the use case. The sample app is available on Github and it's developed as of iOS 9 Beta 5.
From the sample app, it can be clearly seen which system events happen when Slide Over is used, and when Split View is used.
When our app is primary, and Slide Over is used, we get UIApplicationWillResignActiveNotification and AVCaptureSessionDidStopRunningNotification
When Slide Over is used, and our app is secondary, we get UIApplicationWillEnterForegroundNotification and AVCaptureSessionDidStopRunningNotification immediately after that
When Split View is used, on each divider drag, our app gets UIApplicationWillResignActiveNotification.
However, if the Camera is launched when in Split View, it immediately gets AVCaptureSessionDidStopRunningNotification
So, empirically, it looks like AVCaptureSession is immediately stopped when Slide Over or Split View are used.
What's confusing is that UIImagePickerController, which our sample app also supports, exhibits completely different behaviour.
UIImagePickerController isn't stopped when the app goes into Slide Over/ Split View, instead, it functions completely normally. One can normally take a photo in Split View. In fact, two apps, both of which present UIImagePickerController, can work side by side, with UIImagePickerController of the active app being active. (You can try that by running our sample app, and Contacts app -> New Contact -> Add photo)
With all this in mind, our questions are the following:
If AVCaptureSession is immediately paused when Slide Over and Split View are used, is it a good idea to monitor AVCaptureSessionDidStopRunningNotification, and present a message "Camera Paused" to the user, so that he clearly knows that the app isn't performing scanning?
Why is behaviour of UIImagePickerController different than AVCaptureSession?
Can we expect from Apple than in future beta versions behaviour of AVCaptureSession changes to match UIImagePickerController?
In case you haven't found out yet. After some more investigation I can now answer your first question:
If AVCaptureSession is immediately paused when Slide Over and Split
View are used, is it a good idea to monitor
AVCaptureSessionDidStopRunningNotification, and present a message
"Camera Paused" to the user, so that he clearly knows that the app
isn't performing scanning?
The notification you actually want to observe is this one: AVCaptureSessionWasInterruptedNotification
And you want to check for the newly introduced in iOS9 reason: AVCaptureSessionInterruptionReason.VideoDeviceNotAvailableWithMultipleForegroundApps
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
self.addObserverForAVCaptureSessionWasInterrupted()
}
func addObserverForAVCaptureSessionWasInterrupted()
{
let mainQueue = NSOperationQueue.mainQueue()
NSNotificationCenter.defaultCenter().addObserverForName(AVCaptureSessionWasInterruptedNotification, object: nil, queue: mainQueue)
{ (notification: NSNotification) -> Void in
guard let userInfo = notification.userInfo else
{
return
}
// Check if the current system is iOS9+ because AVCaptureSessionInterruptionReasonKey is iOS9+ (relates to Split View / Slide Over)
if #available(iOS 9.0, *)
{
if let interruptionReason = userInfo[AVCaptureSessionInterruptionReasonKey] where Int(interruptionReason as! NSNumber) == AVCaptureSessionInterruptionReason.VideoDeviceNotAvailableWithMultipleForegroundApps.rawValue
{
// Warn the user they need to get back to Full Screen Mode
}
}
else
{
// Fallback on earlier versions. From iOS8 and below Split View and Slide Over don't exist, no need to handle anything then.
}
}
}
override func viewWillDisappear(animated: Bool)
{
super.viewWillDisappear(true)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
You can also know when the interruption was ended by observing:
AVCaptureSessionInterruptionEndedNotification
Answer based on these two links:
http://asciiwwdc.com/2015/sessions/211
https://developer.apple.com/library/ios/samplecode/AVCam/Introduction/Intro.html
Since iOS 16.0+, it's possible to use isMultitaskingCameraAccessEnabled flag.
Reference
Since iOS 13.5+ and iPadOS 13.5+, it's possible to use entitlement com.apple.developer.avfoundation.multitasking-camera-access, allowing the app to continue using the camera while running alongside another foreground app.
Reference
More information about accessing the camera while multitasking
here

Resources