Load Data Before Adding ViewControllers to UITabBarController - ios

In my application, once the user logs in from the LoginViewController, he is directed to the ProfileTabBarController.
ProfileTabBarController is a subclass of UITabBarController.It consists of three view controllers all of which need a reference to an instance of Profile.
When the ProfileTabBarController is pushed, it loads the user's profile. Once the profile loads successfully, it sets a reference to profile on each of its view controllers and then adds them as tab items.
The reason I chose this approach is that if the user launches the application and his token hasn't expired, the application should go directly to the ProfileTabController; the user need not login again. Rather than duplicate the code for loading the profile in both AppDelegate and LoginViewController, I felt it was better to encapsulate it in ProfileTabBarController.
The problem with this approach is that during the segue, the UITabBarController appears black as no view controller has been set. I managed to remedy this by creating a LoadingViewController and setting it initially as the only UIViewController of ProfileTabController
My question is whether there is a better approach to solving this problem. I don't really like the idea of having a UIViewController with no other purpose then to display a loading icon.

Presenting a tab view controller with no views doesn't make much sense, so I think your solution of giving it a view controller to handle this loading state makes perfect sense. Throw in some cool animation to keep the user entertained.
It's also possible that something could go wrong and the profile may not load properly. This transitional view controller seems like a good place to put the code to deal with possible errors.

I was able to eliminate the Loading ViewController by making use of NSNotificationCenter.
ProfileTabBarController adds the three ViewControllers and then begins loading the profile.
override func viewDidLoad() {
super.viewDidLoad()
self.setupUI()
self.setupViewControllers()
self.loadProfile()
}
Once loading completes, the ProfileTabBarController emits a ProfileDidLoad notification.
let completion = { (profile: Profile?) in
MBProgressHUD.hideHUDForView(self.view, animated: true)
if let profile = profile {
self.profile = profile
NSNotificationCenter.defaultCenter().postNotificationName(Notification.ProfileDidLoad, object: self.profile)
} else {
UIAlertView(
title: "",
message: NSLocalizedString("Error loading profile", comment: ""),
delegate: nil,
cancelButtonTitle: "OK"
).show()
}
}
The ViewControllers observe the ProfileDidLoad notification:
func setupNotificationObserver(){
NSNotificationCenter.defaultCenter().addObserver(self, selector: "profileDidLoad:", name: Notification.ProfileDidLoad, object: nil)
}
#objc func profileDidLoad(notification: NSNotification){
if let profile = notification.object as? Profile{
self.profile = profile
}
}
In this way, the view controllers are immediately displayed after login without the need of a loading screen.

Related

IOS swift UIBarButtonItem action

I've a table view with navigation controller embedded in. I've added a UIBarButtonItem (add) button. When I click this button it opens a new view where user enters the data and submits it (which makes a web service call) and returns back to the previous view. This navigation happens as shown below,
func addTapped(_ sender:UIBarButtonItem) {
print("Called Add")
let vc = (storyboard?.instantiateViewController( withIdentifier: "newNote")) as! newNoteVC
self.navigationController?.pushViewController(vc, animated: true)
}
And in new view I do following,
#IBAction func saveButton(_ sender: UIButton) {
if (self.noteDescription.text?.isEmpty)! {
print("Enter missing note description")
return
} else {
let desc = self.noteDescription.text
self.uploadNote(noteText: desc!, noteDate: self.dateInMilliseconds)
self.navigationController?.popViewController(animated: true)
}
}
This way a record gets saved and a view gets popped from the navigation controller stack but only thing I don't how to do is refresh the table view data in the parent view (where I might need to make a new http service call to read all the records).
I hope I'm able to explain the issue? Any help is appreciated.
As mentioned in the comments, making a service call just to update the tableview might be a overkill. However, if this is the business scenario which needs to be implemented, you can do the same in
func viewWillAppear
in the parent view controller. You can make the service call in this method and reload the table view with the data.
You would also need to check the overall navigation of the application as making service calls in viewWillAppear method is not a good approach as these gets called everytime the view is shown. For Ex: If coming back from a navigated view then also the method is called.

ViewDidLoad is always called

I just recently started to use Swift and am facing a "weird" bug with the viewDidLoad method.
My very simple app currently only has 2 viewcontrollers:
MainViewController, which is the Apps entry point and serves as an overview for the data which has already been created. It also provides the option to add new data, which would trigger a segue to
DataViewController, which provides the UI to create new data, after which it goes back to the MainViewController
Now my issue is that the viewDidLoad method of MainViewController is always called whenever the MainViewController appears (At the start of the app and every time the DataViewController disappears). Particularly, the msg "MainViewController newly created" is always printed.
Even worse, it seems that my app is "secretly" resetting. To show this I have defined the class variable "createView" in my MainViewController which is true by default, and is set to false during the viewDidLoad (the only place where this variable is called/set). However the msg "MVC newly created" is still always printed in the output after the MainViewController shows up. How can that be? Why / how is createView reset to true?
Hope this snippet is enough to find the issue. Otherwise, let me know if something is missing.
Thanks for your help!
override func viewDidLoad()
{
super.viewDidLoad()
if (createView)
{
determineArraySize()
createDataArray()
print("MainViewController newly created")
createView = false
}
else {print("Nothing happened")}
}
As #moritz mentioned in the comments, check out the way you present the DataViewController in your storyboard.
If the view is presented modally, you should call:
dismiss(animated: true, completion: nil)
If the view is presented using a show seque, you should call:
_ = navigationController?.popViewControllerAnimated(true)

How to manage loginViewControllers in an app programmatically?

I am trying to use Firebase to create a mini social media. There are currently two components, a loginRegisterViewController, which handles login and registration, and a TabbarViewcontroler, which presents the main content.
In appDelegate's didFinishLaunching method, i set the TabbarViewcontroler as the rootView controller
let rootviewController = TabbarController()
window?.rootViewController = rootviewController
and in the TabbarController's viewDidLoad method, i implement the following code to see if a LoginViewController should be presented or not.
class TabbarController: UITabBarController {
var handle : FIRAuthStateDidChangeListenerHandle?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// check if the user is logged in or not.
handle = FIRAuth.auth()!.addStateDidChangeListener(){ auth, user in
if user == nil {
self.present(loginRegisterViewController(), animated: true, completion: nil)
}else{
self.setupTabbarController()
}
}
}
}
If the user is logged in, we setup the tabbarViewController, if the user is not, we present the loginRegisterViewController on top of the tabbarViewController.
However, every time I register a new user and dismiss the loginRegisterViewController, I will get an empty tabbarViewController.
I understand that this happens because of the if-else condition in the viewDidLoad method of the tabbarViewController.
Since initially there is no user logged in, the
self.setupTabbarController()
method is thus not called.. Therefore, after i register a new user and dismiss the loginRegisterViewController, i get an empty tabbarViewController.
But, how do I go to solve this? Or is there a better way of checking if a user is logged in or not to present the mainContentViewController or LoginRegisterViewController?
Thanks in advance.
Edit : The setupTabbarViewController is a function to setup the tabbarController
func setupTabbarController(){
let shopControler = ViewController()
let shopNavigationControler = UINavigationController(rootViewController: shopControler)
shopNavigationControler.tabBarItem.title = "Shop"
shopNavigationControler.tabBarItem.image = UIImage(named: "Shop_Tabbar_Image")
viewControllers = [newsfeedNavigationControler, shopNavigationControler]
}
as you mentioned correctly we would need to call self.setupTabbarController() again, after the login state changes.
I usually go with this kind of implementation:
Create a class AuthManager or something like that. This has the users loginState and a function called configureRootViewController.
Declare a static sharedInstance.
In the AppDelegate - didFinishLaunchWithOptions:
Your call would look something lie this:
AuthManager.sharedInstance.configureRootViewController()
All that configureRootViewController() does is it checks the login state and configures the correct rootViewController.
Every time your login state changes you want to call this method again.
I hope that helped. Still rather early to give precise answers. :-)
Have a great day.

Pass data between three viewController, all in navigationController, popToRootView

The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.

Right way or event to choose what view load in swift

I'm working in an app that logs in an user if there isn't another user already logged in at launch time. This way the first view to appear should be the Login View. But in the case there is a logged user already, the first view appearing should be the main menu. Im handling this with the viewWillAppear function and it's working, but I don't know if this is the correct approach or how it should be handle in this situations.
Here is my code. My first view is MainMenuVC in which I control if there is a logged user or not, then I choose if stay in main menu view or push my login view.
class MainMenuVC: UIViewController {
override func viewWillAppear(animated: Bool) {
if (UserMgr.users.count == 0){
var vc1:LoginVC = self.storyboard?.instantiateViewControllerWithIdentifier("LoginView") as LoginVC
self.navigationController?.pushViewController(vc1, animated: false)
}
else
{
//I do nothing so this view is loaded
}
}
I don't know if i should use another ViewController and implement the function loadView() to decide what view load, but the problem is make that view work with the story board and my navigation controller.
Any suggestions?
Basically you will have two different view controllers, one for the login screen (VCLogin) and one for the main menu (VCMainMenu). Now, in your AppDelegate there are methods which are called, when the app launches respectively when it appears. So, place the code checking whether a user is logged in there and make the appropriate view controller the root view controller, e.g.
let navigationController = window.rootViewController as UINavigationController
navigationController.rootViewController =
userIsLoggedIn ? mainMenuViewController : loginViewController

Resources