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.
Related
I want to adopt the state restoration in my app. I've opted out of using SceneDelegate because my app is not supposed to be running in multiple windows.
So I've implemented the necessary methods in my AppDelegate
func application(_ application: UIApplication, shouldSaveSecureApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldRestoreSecureApplicationState coder: NSCoder) -> Bool {
return true
}
But every time I quit the app I get this message in the debugger
No windows have a root view controller, cannot save application state
I setup my window programmatically at the launch of app like this
func configureWindow() {
let window = UIWindow(frame: UIScreen.main.bounds)
let launchViewController = Storyboard.launch.instantiate() as LaunchViewController
launchViewController.viewModel = LaunchViewModel(context: context)
window.rootViewController = RootNavigationController(rootViewController: launchViewController)
window.makeKeyAndVisible()
self.window = window
}
So I don't see how is it possible that at the time the app gets killed it has no rootViewController.
This definitely seems like a bug to me, but has anyone encountered such issue?
I've opted out of using SceneDelegate because my app is not supposed to be running in multiple windows.
Check this answer, it links to the article, that says
The best way to implement UI state restoration is to make your app scene-based
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.
I followed the tutorial by google on https://firebase.google.com/docs/notifications/ios/console-topics#receive_and_handle_topic_messages to subscribe to a Firebase topic on my iOS app.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
FIRMessaging.messaging().subscribeToTopic("/topics/Notifications")
let homeViewController = UINavigationController(rootViewController: HomeViewController())
UINavigationBar.appearance().translucent = false
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.rootViewController = homeViewController
window?.makeKeyAndVisible()
return true
}
However, when I send a topic push notification out from the Firebase console. I could not receive any push notifications. But when I send out push notification to user segment from the console, the push is working perfectly. When I check the Xcode console, I am seeing this FIRMessaging error.
2016-05-31 11:11:47.893: <FIRMessaging/WARNING> Cannot subscribe to topic: /topics/Notifications with token: (null)
I've tried to search for this error but have no luck finding anything. I am not sure if this is the problem that is causing my app to not receive any push from topics.
Does anyone have this issue and know how to solve it?
Looks like maybe you're calling subscribeToTopic too early.
First, before you set up any Firebase call, make sure you call
FIRApp.configure()
That will ensure that all Firebase services are properly set up and initialized.
Next, you're going to need to wait just a bit to subscribe to topics. Your client needs to first register your app with both APNs and FCM to ensure that it can receive notifications. That involves a network call, which means you can't subscribe to topics when your app first launches.
Instead, I'd recommend putting that code into your application:didRegisterUserNotificationSettings handler instead. Something like this:
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
NSLog(#"Hooray! I'm registered!");
[[FIRMessaging messaging] subscribeToTopic:#"/topics/cool_users"];
}
Edit: And the Swift version...
func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {
print("Hooray! I'm registered!")
FIRMessaging.messaging().subscribeToTopic("/topics/swift_fans")
}
The accepted solution did not work for me. The token is not always available when application:didRegisterUserNotificationSettings: is called.
For example if application is freshly installed and starts for the first time FIRInstanceID.instanceID().token() returns nil.
You need to make sure application calls subscribeToTopic: after the token is available.
I ended up with creating a helper object that enqueues subscribeToTopic:, unsubscribeFrom: calls and executes them in FIFO order after the token arrives.
class FIRMessagingHelper {
private let queue: OperationQueue
init() {
queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.addOperation(TokenReadyOperation())
}
func subscribeTo(topic: String) {
queue.addOperation {
OperationQueue.main.addOperation({
FIRMessaging.messaging().subscribeToTopic(topic)
})
}
}
func unsubscribeFrom(topic: String) {
queue.addOperation {
OperationQueue.main.addOperation({
FIRMessaging.messaging().unsubscribeFromTopic(topic)
})
}
}
}
TokenReadyOperation waits until the token appears. AsynchronousOperation is used as the base class to minimize boilerplate.
class TokenReadyOperation : AsynchronousOperation {
override init() {
super.init()
NotificationCenter.default.addObserver(self,
selector: #selector(TokenReadyOperation.tokenRefreshed(notification:)),
name: .firInstanceIDTokenRefresh,
object: nil)
}
override func didStart() {
finishIfTokenAvailable()
}
private func finishIfTokenAvailable() {
guard FIRInstanceID.instanceID().token() != nil else { return }
markFinished()
}
/// Posted every time token changes
#objc private func tokenRefreshed(notification: Notification) {
finishIfTokenAvailable()
}
}
Few things to keep in mind:
App must call FIRApp.configure() or FIRApp.configureWithOptions(_:) prior making any Firebase calls (as Todd Kerpelman mentioned)
subscribeToTopic:, unsubscribeFrom: are not thread safe and must be executed on main thread
Topic names has to be in "/topics/*" format (as henmer mentioned)
Make sure to use different configuration plist for debug and App Store release of your app. See FIRApp.configureWithOptions(_:) documentation.
Date & Time should be current, otherwise the token may not be delivered.
Make sure to use the newest framework version. I had issues with notification delivery with the SDK released around January 2017.
My problem was not solved by calling subscribeToTopic after
func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {
instead it worked by calling subscribeToTopic after
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
this function get called when you get your push token from APNS not firebase.
Xcode 8.3.2
Swift 3.0
I am developing an ios app using the Facebook SDK to login.
I have set a LogInViewController as the initial View Controller in the Storyboard, from where the user logins using the FB account.
I have another ViewController which is loaded correctly once the user logs in.
In the AppDelegate file I am checking for currentAccessToken and if it is not nil, I am loading directly the second ViewController, because the user is already logged in.
However, the currentAccessToken is always nil if I quit the app and relaunch it. It only works if I press the home button and re-open the app while it's still running in the background.
Here are the details in the code:
AppDelegate.swift
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.customNavigationBar()
if (!isIcloudAvailable()) {
self.displayAlertWithTitle("iCloud", message: "iCloud is not available." +
" Please sign into your iCloud account and restart this app")
return true
}
if (FBSDKAccessToken.currentAccessToken() != nil) {
self.instantiateViewController("MapViewController", storyboardIdentifier: "Main")
}
return FBSDKApplicationDelegate.sharedInstance().application(application, didFinishLaunchingWithOptions: launchOptions)
}
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
return FBSDKApplicationDelegate.sharedInstance().application(
application,
openURL: url,
sourceApplication: sourceApplication,
annotation: annotation)
}
func applicationWillResignActive(application: UIApplication) {
FBSDKAppEvents.activateApp()
}
func applicationDidBecomeActive(application: UIApplication) {
FBSDKAppEvents.activateApp()
}
LogInViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
// Listen to the Facebook notification and when received, execute func handleFBSessionStateChangeWithNotification
NSNotificationCenter.defaultCenter().addObserver(self, selector:"handleFBSessionStateChangeWithNotification:", name: "SessionStateChangeNotification", object: nil)
}
func handleFBSessionStateChangeWithNotification(notification: NSNotification) {
// Switch to MapViewController when logged in
if ((FBSDKAccessToken.currentAccessToken()) != nil) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let mapViewController = storyboard.instantiateViewControllerWithIdentifier("MapViewController") as! MapViewController
self.presentViewController(mapViewController, animated: false, completion: nil)
}
}
I don't know if it is related, but I am also getting a warning for the MapViewController because there is no segue put towards it from the Storyboard:
Warning: Attempt to present MapViewController whose view is not in the
window hierarchy!
The problem is because you are calling for FBSDKAccessToken.currentAccessToken() before having called
FBSDKApplicationDelegate.sharedInstance().application(application, didFinishLaunchingWithOptions: launchOptions)
You can check for the access token anytime after calling the above line.
EDIT: Explanation
The above line lets the Facebook SDK process the launchOptions and extract the necessary information which it will require to recognise and persist the user for the application.
In cases where the user is already logged in, this simply initialises the Facebook SDK which in turn logs in the user on the basis of persisted data.
I spent a nice half-day banging my head against this issue. In spite of making sure all the delegate methods were present, the FBSDKAccessToken.current() always returned nil.
It turns out that this is because Keychain Sharing is not enabled ( Xcode 8, iOS 10). To fix, go to App -> Capabilities -> Keychain Sharing and turn ON.
Once this is done, you have to still go through the authorization process and return back to the app. Everything should be fine after.
If you already tried the upon solutions, but still have the problem, try this.
My co-programmer and I are using the LoginManager().login method provided in Facebook Swift SDK. (Xcode8, Swift 3, iOS 10)
One possible action that caused that issue is when you login successfully and kill the app immediately, then open your app again and the AccessToken.current returns nil. After login, if you wait 20 seconds or longer (we are not sure the exact waiting time, we waited for 7-20 seconds) and kill the app, the problem seemed to be solved.
We are not sure why this is happened, but this solved our problem, and our guess is that there may be a network delay or a bug.
For those who still having the problem even after set the FBSDKApplicationDelegate.sharedInstance() on the appdelegate, turn out if u get error on your Xcode console like this one
Falling back to storing access token in NSUserDefaults because of simulator bug
it is simulator bug and i try using real device and it work, the access token is not nil again..
so maybe answer #victor comment about this one on #Kal answer..
In my iPhone app need to display a splash screen (displays company logo and some info) for 2-3 seconds before load the first page.
Also the app needs to decide which page should load as the first page in here (according the 'initial setup completion' level by the user).
I am using 'Swift' as the programing language and 'Universal StoryBoard' to design interfaces...
I have seleted the Main.storyboard as the Launch Screen File. In the ViewController class have implemented following logic
override func viewDidAppear(animated: Bool) {
NSLog("Before sleep...")
sleep(2)
NSLog("After sleep...")
self.controllNavigation()
}
func controllNavigation() -> Void {
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var nextViewController
if (Condition1)
{
nextViewController = storyBoard.instantiateViewControllerWithIdentifier("MainMenu") as! MainMenuViewController
}
else
{
nextViewController = storyBoard.instantiateViewControllerWithIdentifier("UserSetup") as! UserSetupViewController
}
self.presentViewController(nextViewController, animated: true, completion: nil)
}
All works ok but While waiting with sleep(2), refresh page sort of a thing happens. I am not sure if this is the best way to do. Like to hear ideas. Thanks
Use the delay in app delegate instead of viewController's viewDidAppear.
Use as:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
NSLog("Before Sleep");
sleep(2);
NSLog("After sleep");
return true
}
Doing this will allow your splash screen to stay till 2 seconds and then you can use following in your view controller:
override func viewDidAppear(animated: Bool) {
self.controllNavigation()
}
Hope this helps :)
I came across similar answers which mentioned sleep(), however it doesn't seem appropriate to use as it blocks the main thread.
I came up with this solution if you are using the default LaunchScreen.storyboard in Xcode 7.2:
func showLaunchScreen() {
let launchScreen = UIStoryboard(name: "LaunchScreen", bundle: nil)
let launchView = launchScreen.instantiateInitialViewController()
self.view.addSubview(launchView!.view)
let timeDelay = dispatch_time(DISPATCH_TIME_NOW, Int64(2 * Double(NSEC_PER_SEC)))
dispatch_after(timeDelay, dispatch_get_main_queue()) {
UIView.animateWithDuration(0.5, animations: { _ in
launchView?.view.alpha = 0.0
}) { _ in
launchView!.view.removeFromSuperview()
}
}
This will show the launch screen for another 2 seconds, then fade it out over 0.5 seconds. You can call you other function to load the 'default' VC in the completion handler.
I have solved this issue with this:
On my project I set the desired storyboard of splash (in my case Splash.storyboard) as Default Screen -> Targets/ (your target)/ Info/ Launch Screen Interface File Base Name.
Field on Project Settings
When you've done that, you can insert your logic on method "func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {} " placed on AppDelegate.swift .
If you want to make clean code, you can insert all your navigation related methods on another Class, and call them from AppDelegate.swift .
With this solution, the splash is visible only the required time, and when it finish, you can navigate to the screen you need.
Swift 4 Update 100% working
Just write one line of code
Thread.sleep(forTimeInterval: 3.0)
in the method of didfinishLauching.... in appdelegate class.
Example
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Thread.sleep(forTimeInterval: 3.0)
// Override point for customization after application launch.
return true
}