I have four viewControllers in my application. Now lets suppose user is currently on the second viewController and He terminates the application.
Now how can I save this state, so that when user reopen the application he should be presented with the second viewContoller, and should be able to navigate back to first viewContoller as well.
The solution I have in my mind is to simply save a variable of current screen in userDefaults and then make that viewController a rootViewController. But I know its not the proper solution and I will lose navigation as well.
Please guide me thanks.
If you haven't yet, create a class called BaseViewController that should be extended from all your UIViewControllers:
class BaseViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// always remember last controller shown
NSUserDefaults.standardUserDefaults.setObject(classForCoder(), forKey: "lastController")
}
}
You can save controller class name, an int identifier or somenthing you want.
Then, in app delegate on application start you can read this value and make your custom logic to open the right controller.
if NSUserDefaults.standardUserDefaults().stringForKey("lastController") == MyCustomViewController.classForCoder() {
// open MyCustomViewController
}
Then when user goes back, redirect to the right UIViewController according your application flow.
Related
I have created a login/sign up screen for my application using firebase and now when a user successfully logs in or signs up I want to transfer them to the rest of my application.
I am currently using a segue to do this, so whenever a user enters information that is valid in the firebase database, or whenever they sign up and create their information in the database, a segue takes them to the tab bar controller of the app itself.
I am new to Xcode and don't know if this is a safe thing to do or not. For example, does this create a danger of users being able to get into my actual app without being properly identified in the database if they somehow manipulate the segue into taking them there? I don't want any dangers like this in my app and want to know if there is any other way in which this could, or maybe should, be done.
Thank you very much.
Using a segue is fine, but you might want to consider doing it in reverse.
If the user isn't signed up, when the application is launched, present a modal sign up viewcontroller from the main application viewcontroller. Once they've entered their info and have been validated, do an unwind segue to return to the main application screen.
The reason to do it this way is that it frees the memory used by the login/signup screen. The way you are doing it keeps the login/signup VC in memory the whole time the app is running.
Your main viewController in your UITabViewController would look like this:
class FirstViewController: UIViewController {
var loggedIn = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !loggedIn {
self.performSegue(withIdentifier: "LogInSignUp", sender: self)
}
}
// This is the target of the unwind segue
#IBAction func finishedLoggingIn(_ segue: UIStoryboardSegue) {
loggedIn = true
print("logged in and ready to go!")
}
}
If you uncheck the Animates checkbox in the Attributes Inspector for the "LogInSignUp" segue, the login screen will appear immediately without animation, but it will still drop down once the login is complete (during the unwind segue).
There is no way for users to manipulate a segue. It's up to you, of course, to make sure that users are properly authenticated before taking them into your application.
Upon app start, i have a login screen, which is an instance of LoginViewController and on success, i.e. after my user is successfully logged in, the main content of my app is being shown using the performSegue(withIdentifier identifier: String, sender: Any?) method to an instance of a UITabBarController. Unfortunately i deinit() is not being called on my LoginViewController after the segue. This does not happen due to any reference cycles i might have in my LoginViewController, because i explicitely checked for those.
Since a login screen is only required once upon app start, i was wondering if there is a best practice to be able to make sure that my LoginViewController is being deallocated after the segue?
Maybe using a segue is just not the right way to achive this, because
from what i understand, the conclusion of a similar question Deinit not called on Show Detail segue was the fact, that using a showDetail-segue is the same as using a modal presentation, i.e. both controllers keep a reference to each other and deinit() will not be called in either of them. Is this really ecpected behavior, or am i missing something?
Update: The reason i care so much, is the fact that i am using RxSwift and my LoginViewController is subscribed on several Observables, so after my app leaves the LoginViewController, i want to make sure to set my collected DisposeBag to nil. Would it be finde to do this in the ViewDidDisappear() of my LoginViewController?
I would suggest you to create a custom segue class and override the perform function like this:
class CustomSegue: UIStoryboardSegue {
override func perform() {
source.view.window?.rootViewController = destination
}
}
For my iOS app, I am currently checking if a user is logged in with email and password via firebase in my initial view controller, using the recommended method from the firebase documentation. If a user is not logged in, I then present my login screen as shown below:
class InitialViewController: UIViewController {
var authHandle: FIRAuthStateDidChangeListenerHandle!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hasUserSignedIn()
}
func hasUserSignedIn() {
authHandle = FIRAuth.auth()?.addStateDidChangeListener { [unowned self] auth, user in
if user == nil {
self.perform(#selector(self.presentLogInSignUp), with: nil, afterDelay: 0)
}
}
}
However, when a user is not signed in, they briefly see my initial view controller before the log in controller is presented, which isn't a great user experience. A way to solve this would be to add a check in the app delegate's didFinishLaunchingWithOptions.
Firstly, is the app delegate and didFinishLaunchingWithOptions an OK place to check if a user is logged in with Firebase (would be done after FIRAppConfigure())?. I assume it is, as from my understanding, a user's Firebase logged in state is persisted in the keychain (is that correct?)
Secondly, if the app delegate is an appropriate place for checking a user's logged in state, should I use the recommended way as I do in my initial view controller or the method below? The advantage of latter is that I don't need to worry about having to remove a listener but Firebase docs gives a warning with this method that the auth object may not have finished initialising.
if FIRAuth.auth()?.currentUser != nil {
// User is signed in.
// ...
} else {
// No user is signed in.
// ...
}
Current potential solution:
I use the recommended method in didFinishLaunchingWithOptions and I just remove the handler after using it within didFinishLaunchingWithOptions.
Firebase is awesome and look forward to implementing this correctly.
they briefly see my initial view controller
They are not seeing the initial view controller. They are seeing the view that the initial view controller is controlling. i.e. by the time the code gets to viewWillAppear the view has already been loaded and is ready to be shown which is why it's initially visible before changing to the log in view.
The logic to determine which view should be shown should occur before then.
One option is to determine that in the app delegate and instantiate the appropriate viewController in code in the didFinishLaunchingWithOptions function.
If you look at your Main.storyboard, click the view and then on the right in the attributes inspector, there's a checkbox for Is Initial View Controller. That's the first one to be displayed. If you uncheck that, then you can set it up in code.
See this question and answer
Programmatically set the initial view controller using Storyboards
You can do the check in didFinishLaunchingWithOptions, or viewDidLoad....
Or... alternatively, for anyone who want to keep it in viewWillAppear, because you may have to do that anyway. For example if the user gets their email verified and you come back to the same page, you may have to reaload the user and check isEmailVerified inside of viewWillAppear.
So one way of showing different controllers in general is to embed them in a customised UITabBarController, make sure to hide the actual bar of cause. You can then check the "Auth.auth().currentUser" in the viewWillAppear of the customised UITabBarController and select the page you want to show conditionally (with self.selectedIndex = whatever and make sure not to animate the transition).
I guess the advantage of doing it this way is the flow is clear and if you use the storyboard it's nice and tidy.
I have a modally presented View Controller called ChangeViewController that allows the user to change some information regarding his/her profile. When the user is done, the 'ACCEPT CHANGES' button is pressed and I use an unwind segue to exit out of the ChangeViewController and go back to the main screen.
Is it possible for me to refresh the data of the entire app before performing this unwind segue? As of now, the information on the home screen and other screens remains unchanged after user modifications. Only after an app reboot does the information update. Is there a way to programmatically 'reboot' so all View Controllers are up to date? More specifically, is there a way to call the viewDidLoad functions of all View Controllers so their data is updated?
Solutions in Swift preferred. Thank you!
I oftentimes updateUI after a user changes with a function like this. This is pretty simple, and you just call the function whenever your UI is updated.
func updateUI() {
// Redraw your labels, update your UIElements, do what you have to do
}
A way you can call this function from a modally presented ViewController without closing the app is with delegation, since modal presentation does not throw the old ViewController out of the stack and heap, Delegation works like this:
In your modal controller:
protocol ChangeViewControllerDelegat: class {
func updateUI(sender:UIButton)
}
class ChangeViewController: UIViewController {
weak var delegate: ChangeViewControllerDelegat?
func opChangingUserSettings() {
// Change settings with your code
// tell your ViewController to do it.
delegate?.updateUI()
}
in your mainVC
class MainViewController: UIViewController, ExtensionViewControllerDelegate {
func updateUI() {
// Redraw your labels, update your UIElements, do what you have to do
}
}
Hope that helps!
By the way, are you trying to change language by any chance on the fly? If so, I can show you how to do that. If not, and If I understand your question, this should work.
Well, you can use the local notification for your scenario.
As in, when the user is done with the changes and presses the button to accept the changes, there you can post a local notification to reload the data.
And you can listen to that notification in all of your viewControllers and reload the data there, like if it is a table view then you can simply call the reloadData method on tableview to achieve it.
I'm trying to figure out how to navigate around my app. But i'm a little lost.
I have a UIViewController that loads some data, then displays the data in a CollectionView. Then I have another UIViewController for the detailed view. I then trigger a segue to go to it, I pass the data etc.
self.performSegueWithIdentifier("detailViewSeque", sender: nil)
But the part i'm lost on is getting back to my main view, if I just trigger another segue then it loads all the data / view again. The data has already been loaded once, I really don't want to keep loading it.
I feel like I'm doing things wrong, that theres some super obvious way to handle this scenario.
Could someone point me in the right direction?
This is good situation to use an unwind segue (for more information: What are Unwind segues for and how do you use them?). Here's how to setup one up:
Firstly, create an #IBAction in the view controller you want to segue to, that takes a UIStoryboardSegue as its only argument. For example:
#IBAction func unwindToHere(segue: UIStoryboardSegue) {
// If you need you have access to the previous view controller
// through the segue object.
}
Secondly, you need to create the unwind segue in IB. To do this ctrl-drag from the view controller you want to segue from, to Exit and select the unwindToHere method:
Thirdly, you need to give your segue and identifier. To do this select your segue (see below - your segue will not be visible like normal segues); then use the Attribute Editor to give your segue an identifier.
Now you can use your segue. On the view controller you want to segue from, call:
self.performSegueWithIdentifier("YourID", sender: self)
To rephrase your needs "I have data that I need to keep around somewhere that isn't associated with a view controller".
You have a few options here. Your goal is basically to store it somewhere that isn't going to go out of memory.
The AppDelegate gets used for this purpose a lot but Singleton variable works as well.
I would personally create a singleton, say CatPictureRetriever with
private let _CatPictureRetriever SharedInstance = CatPictureRetriever()
class CatPictureRetriever {
static let sharedInstance = CatPictureRetriever()
var catPictures : NSArray?;
func gimmeCatPictures -> NSArray? {
return catPictures
}
}
Now you can get your pictures though your CatPictureRetriever anywhere
var pictures = CatPictureRetriever.sharedInstance.gimmeCatPictures()