AVCaptureDevice.requestAccess presents unexpected behavior with a UINavigationController - ios

Working with Xcode 10.1 and Swift 4.2
I have a complex app that uses a UINavigationController implemented in the AppDelegate.
The rootViewController of the navigationController is a DashboardController() class (subclass of UIViewController)
The DashboardController implements a left menu drawer using several ViewControllers (with self.addChild(viewController))
Everything works fine, except when I need to push a viewController to present a BarCodeScannerView().
The barebone barCodeScannerView can be pushed and popped as expected.
The problems arises when I request access to the camera (only the first time).
As soon as I present the Device.requestAccess(for:) as follow: the viewController is popped and the previous view (rootViewController) is presented. (Still with the "App would like to access the camera" AlertView)
func requestCameraAccess() {
AVCaptureDevice.requestAccess(for: AVMediaType.video) { granted in
if granted {
self.launchScanner()
} else {
self.goBack()
}
}
}
If I click "OK" The system will register that the access was granted, but the
applicationDidBecomeActive (in the AppDelegate) is called after aprox 1 second. I have some initializers in applicationDidBecomeActive, and they all are executed again. And after a quick delay, everything works fine.
BTW: applicationWillResignActive, applicationDidEnterBackground and applicationWillEnterForeground are NOT called. So it is clear that this is not part of an App LifeCycle.
Any idea what might me going on here? What can make the system call applicationDidBecomeActive within the app? and still keep everything running?
Thx in advance...
UPDATE After reading the comments, I was able to isolate the issue #2 as follows:
A simple/barebones project with a UINavigationController with a dashboardViewController as rootViewController. The dashboardViewController pushes a CameraViewController() in viewDidLoad(). The cameraViewController requests access to the camera. When clicking OK, the call to applicationDidBecomeActive is triggered.
The full project is attached. (except the "Privacy - Camera Usage Description" key in the .plist.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? = UIWindow()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let dashboardViewController = DashboardViewController()
window?.rootViewController = UINavigationController(rootViewController: dashboardViewController)
window?.makeKeyAndVisible()
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
print("applicationDidBecomeActive")
}
func applicationWillResignActive(_ application: UIApplication) {}
func applicationDidEnterBackground(_ application: UIApplication) {}
func applicationWillEnterForeground(_ application: UIApplication) {}
func applicationWillTerminate(_ application: UIApplication) {}
}
class DashboardViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
let cameraVC = CameraViewController()
self.navigationController?.pushViewController(cameraVC, animated: true)
}
}
import AVFoundation
class CameraViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
AVCaptureDevice.requestAccess(for: AVMediaType.video) { granted in
if granted {
print("Access granted")
}
}
}
}

I'd say the problem is just with your testing procedure. When I run your code with a print statement in applicationWillResignActive, this is what I see:
applicationDidBecomeActive
applicationWillResignActive
Access granted
applicationDidBecomeActive
That seems completely in order and normal. It would have been weird to get a spurious didBecomeActive, but that is not what's happening; we resign active and then become active again, which is fine. You should expect that at any time your app can resign active and become active again. Many things in the normal lifecycle can cause that, and the presentation of an out-of-process dialog like the authorization dialog can reasonably be one of them. You should write your code in such a way as to cope with that possibility.

Related

Making Offline Mode work across all screens in iOS app

I am trying to setup a clean way of handling when a user goes offline, currently if a user is offline I am pushing a new screen to the top of the stack with a message to connect to the internet. This is working perfectly for the one screen it is setup on, but I want this code to work on any screen the user is on. I am trying to put this into the app delegate and have the code shared across all screens but it is not working. The end goal is if the user is on any screen in my app and loses connection, to show the offline view controller - currently this is only working on the home screen.
Also I do not want to use Alamo / Firebase or any other 3rd party to handle this if possible.
This is the home screen code and everything working as expected:
import UIKit
class ViewController: UIViewController, UIAlertViewDelegate {
let reachability = try! Reachability()
override func viewDidLoad() {
super.viewDidLoad()
// When network is unreachable, present offline view controller
reachability.whenUnreachable = { _ in
let vc = self.storyboard?.instantiateViewController(withIdentifier: "OfflineViewController")
vc?.modalPresentationStyle = .fullScreen
self.present(vc!, animated: true, completion: nil)
}
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
}
}
This is the code for the offline view controller - it is also working as expected
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 tapTryAgain(_ sender: Any) {
reachability.whenReachable = { reachability in
self.dismiss(animated: true, completion: nil)
}
}
}
Now when I try to put all the code in the app delegate, it does not work -- This the part I need help with - note that I am commenting out the reachability code from home screen when I try this out
import UIKit
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
let reachability = try! Reachability()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Note nothing happens when user goes offline with this code
reachability.whenUnreachable = { _ in
print("Not reachable")
let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let offlineVC = storyboard.instantiateViewController(withIdentifier: "OfflineViewController")
offlineVC.modalPresentationStyle = .fullScreen
let appDelegate = UIApplication.shared.delegate
appDelegate?.window??.addSubview(offlineVC.view)
appDelegate?.window??.bringSubviewToFront(offlineVC.view)
}
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
return true
}
// MARK: UISceneSession Lifecycle
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)
}
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.
}
}
I suppose the problem is that you are trying to use window from AppDelegate, but it will always be nil, since your are using SceneDelegate. Just move this code to SceneDelegate, everything will work as it should then.

application(_ application: UIApplication, didFinishLaunchingWithOptions) not called on start up

When I kill the app by swiping up in the multi-app UI in the simulator and relaunch it the application (didFinishLaunchingWithOptions) method is not called, and every time just the login screen shows up. I do not understand what's going on and it really defeats the purpose of checking whether user is logged in or not from firebase if the method is not even called while launching the app again, would really appreciate some help! (Does this have something to do with SceneDelegate methods I am seeing, from what i understand the didFinishLaunching method should be called regardless when launching an application)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
print("---------appDelegate didFinishLaunchingWithOptions called!---------------")
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = MainViewController()
FirebaseApp.configure()
return true
}
Here's the code for the MainViewController as requested
import UIKit
import Firebase
class MainViewController: UIViewController {
var handle: AuthStateDidChangeListenerHandle?
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.async {
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if user == nil {
print("nil user -----------")
self.perform(#selector(self.showHomeController), with: nil, afterDelay: 3)
} else {
print("non nil user --------")
self.perform(#selector(self.showWelcomeController), with: nil, afterDelay: 3)
}
}
}
}
#objc func showWelcomeController () {
present(WelcomeViewController(), animated: true, completion: nil)
}
#objc func showHomeController () {
present(HomeViewController(), animated: true, completion: nil)
}
}
The ---------appDelegate didFinishLaunchingWithOptions called!--------------- is printed only once, when the project is built and opened in simulator
Disclaimer: I'm very new in iOS app development.
The problem is just the way you "relaunch in the sim". If you kill the app and then tap the app's icon in the simulator, you are no longer running in Xcode; you are running independently. So you don't get any debugging any more; no print messages appear in the Xcode console, you don't stop at breakpoints, etc.
The solution is: relaunch by telling Xcode to build and run again, not by tapping the app's icon in the simulator.
The action of swiping up and killing the application breaks the debugger link. So if you relaunch the application by clicking on the app itself in the simulator, you will no longer have a debugger link with Xcode. That is the reason why you don't see the messages being printed anymore.
You have to relaunch from Xcode to get the launch messages printed once again.
If you have other questions please post them separately. It will be easier to track and respond to by everyone.

Why do we override variables which were initially nil instead of simply setting them?

For example, in MainViewController which is a subclass of UIViewcontroller, I overrode the nib name variable and returned a string to satisfy the argument however in the appDelegate I was able to simply assign an object to the rootViewController variable without using the term override. I understand the keyword override means to rewrite a method which is inherited by the subclass however I'm not understanding it in this sense where we're using variables.
AppDelegate.swift code:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var mainViewController: MainViewController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//so as this function fires we need to put forth some effort.....from inside this function instantiate a view controller
let mainViewController = MainViewController()
//put the view of the ViewController on screen
//mainViewController.show(mainViewController, sender: self)
window?.backgroundColor = UIColor.purple
window?.rootViewController = mainViewController
//set the property to point to the viewController
self.mainViewController = mainViewController
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
MainViewController.swift code:
import Foundation
import UIKit
class MainViewController: UIViewController {
override var nibName: String? {
return "MainViewController"
}
}
nibName is a read-only property - checking the public UIViewController interface shows:
open var nibName: String? { get }
It doesn't make sense for the nibName to be able to change during the lifecycle of your UIViewController instance, so having it as a read-only property ensures it's simply a form of configuration.

open specific view controller upon tapping local notification

title says it all. i've went through a number of posts trying to put together a solution but to no luck..
i have a notification whose name i'm not sure of...
let request = UNNotificationRequest(identifier: "timerDone", content: content, trigger: trigger)
q1: is the name timerDone?
in viewDidLoad():
NotificationCenter.default.addObserver(self,
selector: "SomeNotificationAct:",
name: NSNotification.Name(rawValue: "timerDone"),
object: nil)
and then i have this method:
#objc func SomeNotificationAct(notification: NSNotification){
DispatchQueue.main.async() {
self.performSegue(withIdentifier: "NotificationView", sender: self)
}
}
with this in AppDelegate.swift:
private func application(application: UIApplication, didReceiveRemoteNotification userInfo: Any?){
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "SomeNotification"), object:nil)
}
any ideas how to do this? thanks in advance!
UPDATE: #Sh_Khan
first, i am coding in swift, i tried translating your code from obj-c to swift as:
if (launchOptions![UIApplicationLaunchOptionsKey.localNotification] != nil)
{
var notification =
launchOptions![UIApplicationLaunchOptionsKey.localNotification]
[self application:application didReceiveLocalNotification:notification]; //how to translate?
}
what should the last line be translated into?
when you wrote:
should store a boolean variable in user defaults in didReceiveLocalNotification method and check it in viewDidAppear method of the rootViewcontroller to make the segue and then make it false as the notificationCenter will work only when app is in foreground or in background if it's not yet suspended
let's say the boolean is notiView and we set it to true when we received the local notification and thus the segue will be to a different view controller. is this what you mean?
I found the documentation for user notification a bit confusing and incomplete. The tutorials are better than most other Apple frameworks. However, the tutorials are mostly incomplete and assume that every app implement the notification center delegate inside the AppDelegate. Not!
For many apps handling the notification delegate in a view controller (instead of in the AppDelegate), the view controller would need to be set as the user notification center delegate inside the AppDelegate didFinishLaunchingWithOptions method. Otherwise, your notification firing would not be visible to the notification delegate when your app is launched from the background mode. Your view controller is loaded after the notification fires. You need a way to launch the delegate methods after your view controller has completed its loading.
For example: suppose you are using a split view controller as your initial view controller for your app and you have implemented the split VC's master view controller as your notification delegate, you would need to let UNUserNotificationCenter know that the master VC is its delegate when your application launches (not inside the master VC's viewDidLoad() as most tutorials suggest). Eg,
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let splitViewController = window!.rootViewController as! UISplitViewController
...
if #available(iOS 10.0, *) {
if let masterViewController = splitViewController.viewControllers.first as? MasterViewController {
UNUserNotificationCenter.current().delegate = masterViewController
}
}
}
This would allow iOS to call your notification delegate methods after the master VC is loaded when your app is launched cold or from the background mode.
In addition, if you need your master VC know that it was loaded because a user notification firing (and not from normal loading), you will use the NSUserDefaults to communicated this information. Hence, the AppDelegate would look as follows:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let splitViewController = window!.rootViewController as! UISplitViewController
...
if #available(iOS 10.0, *) {
if let _ = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] {
UserDefaults.standard.set("yes", forKey: kMyAppLaunchedByUserNotification)
UserDefaults.standard.synchronize()
}
if let masterViewController = splitViewController.viewControllers.first as? MasterViewController {
UNUserNotificationCenter.current().delegate = masterViewController
}
}
}
where kMyAppLaunchedByUserNotification is a key you use to communicate with the master VC. In the viewDidAppear() for Master View Controller, you would check User Defaults to see whether it is being loaded because of notification.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 10.0, *) {
if let _ = UserDefaults.standard.object(forKey: kMyAppLaunchedByUserNotification) {
UserDefaults.standard.removeObject(forKey: kMyAppLaunchedByUserNotification)
UserDefaults.standard.synchronize()
//Handle any special thing that you need to do when your app is launched from User notification as opposed to regular app launch here
// NOTE: the notification delegate methods will be called after viewDidAppear() regardless of you doing any special thing here because you told iOS already in AppDelegate didFinishLaunchingWithOptions
}
}
}
I hope this helps you.
If the app is closed and you tapped a local notification then check it in didFinishLaunchingWithOptions method
/// Objective-C
if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey] != nil)
{
UILocalNotification *notification =
launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
[self application:application didReceiveLocalNotification:notification];
}
/// Swift
if (launchOptions![UIApplicationLaunchOptionsKey.localNotification] != nil)
{
var notification =
launchOptions![UIApplicationLaunchOptionsKey.localNotification]
self.application(application, didReceive: notification)
}
note: The viewDidLoad of the rootViewcontroller isn't yet called so observer won't be triggered , so you should store a boolean variable in user defaults in didReceiveLocalNotification method and check it in viewDidAppear method of the rootViewcontroller to make the segue and then make it false as the notificationCenter will work only when app is in foreground or in background if it's not yet suspended . . .
UPDATE: #Shi_Zhang
Yes,this is what I mean

Swift cannot assign value of type 'UINavigationController'

I'm a newbie at Swift so I'm just learning. I am following a tutorial on youtube and I keep getting this error. Let's Build That App : How to build Twitter.'Application windows are expected to have a root view controller at the end of application launch'
For the window?.rootViewController = UINavigationController it says
cannot assign value of type 'UINavigationController' to type
UIViewController?' Expression of type'(rootViewController:
HomeController)' is unused
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// ignore storyboard.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
let homeController = HomeContoller(collectionViewLayout:
UICollectionViewFlowLayout())
window?.rootViewController = UINavigationController
(rootViewController: homeController)
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
Remove the line break between these two lines:
window?.rootViewController = UINavigationController
(rootViewController: homeController)
The first line:
window?.rootViewController = UINavigationController
produces this error:
Cannot assign value of type 'UINavigationController.Type' to type 'UIViewController?'
and the second line:
(rootViewController: homeController)
produces this warning:
Expression of type '(rootViewController: HomeController)' is unused
If you remove the line break, both will go away:
window?.rootViewController = UINavigationController(rootViewController: homeController)
try this:
self.window = UIWindow(frame: UIScreen.main.bounds)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let initialViewController = storyboard.instantiateViewController(withIdentifier: "navigationController")
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
Note: You need to put the storyboard id "navigationController" to your navigationController

Resources