how to start another task once async call is completed - ios

Currently, I am using firebase. I populate my table using firebase async call observeEventType. However now I am also using BTNavigationDropdownMenu and I would need to populate my title from the database. As firebase query is async, how can I make it such that only when it is completed then BTNavigationDropdownMenu will be fired?
This is the call I make at viewDidAppear to retrieve data from firebase
_ = dataRef.observeSingleEventOfType(.Value, withBlock: { (snapshotOne) in
self.titleList.insert("\(snapshotOne.key)", atIndex: 0)
})
This is supposed to fire after data is retrieved
let menuView = BTNavigationDropdownMenu(navigationController: self.navigationController, title: titleList.first!, items: titleList)
self.navigationItem.titleView = menuView
The error that I am getting is unwrapping a nil in titleList, which of course is the case since titleList has not been populated by firebase.
I tried 2 things which failed. Firstly, I placed the BTNavigationDropdownMenu codes in my firebase call. But that gave me a memory warning as the codes kept being called.
Secondly I tried using dispatch_async and serial queue but it didnt work since I was queueing a async code within an async code, which means my queue fired the firebase async codes and moved to my BTNavigationDropdownMenu before firebase responded.

Simply set the titleView inside the async completion closure of value event
dataRef.observeSingleEventOfType(.Value, withBlock: { (snapshotOne) in
self.titleList.insert("\(snapshotOne.key)", atIndex: 0)
let menuView = BTNavigationDropdownMenu(navigationController: self.navigationController, title: titleList.first!, items: titleList)
self.navigationItem.titleView = menuView
})

I think your best bet is to subclass UINavigationController to solve your memory problems. The menuView is getting initialized within UIViewController but then added as a subview of navigationController. So when the ViewController should dealloc, The menuView hasn't dealloc'd because its still on the navigationController, while still having a reference to UIViewController so your UIViewController cannot dealloc either. Its not on the navigationStack, but still exists in memory creating your issue. Here is a quick subclass.
class MyNavController : UINavigationController {
var menuView : BTNavigationDropdownMenu = BTNavigationDropdownMenu()
override func viewDidLoad() {
super.viewDidLoad()
//Do additional setup like adding to superView or whatever else you need.
titleView = menuView
}
}
In firebase completion block
_ = dataRef.observeSingleEventOfType(.Value, withBlock: { (snapshotOne) in
//Individually set all of the values of your menuView here.
if let navVC = self.navigationController as? MyNavController {
navVC.menuView.title = titleList.first
}
})
The key here is that menuView now only has a reference to the navBar so will deallocate itself with the navBar and your VC can now dealloc on its since menuView no longer has a reference to it. So while creating weak reference could solve your memory issues, It makes more sense for the life-cycle of menuView to follow the life cycle of your navController if it lives on the navController

Related

How to prevent timer reset using pushViewController method?

I'm trying to keep a timer running even if I switch view controllers. I played around with the Singleton architecture, but I don't quite get it. Pushing a new view controller seems a little easier, but when I call the below method, the view controller that is pushed is blank (doesn't look like the view controller that I created in Storyboards). The timer view controller that I'm trying to push is also the second view controller, if that changes anything.
#objc func timerPressed() {
let timerVC = TimerViewController()
navigationController?.pushViewController(timerVC, animated: true)
}
You need to load it from storyboard
let vc = self.storyboard!.instantiateViewController(withIdentifier: "VCName") as! TimerViewController
self.navigationController?.pushViewController(timerVC, animated: true)
Not sure if your problem is that your controller is blank or that the timer resets. Anyway, in case that you want to keep the time in the memory and not deallocate upon navigating somewhere else I recommend you this.
Create some kind of Constants class which will have a shared param inside.
It could look like this:
class AppConstants {
static let shared = AppConstants()
var timer: Timer?
}
And do whatever you were doing with the timer here accessing it via the shared param.
AppConstants.shared.timer ...
There are different parts to your question. Sh_Khan told you what was wrong with the way you were loading your view controller (simply invoking a view controller’s init method does not load it’s view hierarchy. Typically you will define your view controller’s views in a storyboard, so you need to instantiate it from that storyboard.)
That doesn’t answer the question of how to manage a timer however. A singleton is a good way to go if you want your timer to be global instead of being tied to a particular view controller.
Post the code that you used to create your singleton and we can help you with that.
Edit: Updated to give the TimeManager a delegate:
The idea is pretty simple. Something like this:
protocol TimeManagerDelegate {
func timerDidFire()
}
class TimerManager {
static let sharedTimerManager = TimerManager()
weak var delegate: TimeManagerDelegate?
//methods/vars to manage a shared timer.
func handleTimer(timer: Timer) {
//Put your housekeeping code to manage the timer here
//Now tell our delegate (if any) that the timer has updated.
//Note the "optional chaining" syntax with the `?`. That means that
//If `delegate` == nil, it doesn't do anything.
delegate?.timerDidFire() //Send a message to the delegate, if there is one.
}
}
And then in your view controller:
//Declare that the view controller conforms to the TimeManagerDelegate protocol
class SomeViewController: UIViewController, TimeManagerDelegate {
//This is the function that gets called on the current delegate
func timerDidFire() {
//Update my clock label (or whatever I need to do in response to a timer update.)
}
override func viewWillAppear() {
super.viewWillAppear()
//Since this view controller is appearing, make it the TimeManager's delegate.
sharedTimerManager.delegate = self
}

How can I manage and free memory through ViewControllers

I'm in front of a big issues, and the only reason I can't find a solution is because my lack of knowledge about swift and memory management in swift. So here is my concerns. I'm working in swift 4.0 and iOS 9.3
I'm actually making a picture gallery app with login/logout. Basic application.
I'm working in cleanSwift So I don't have those HUGE ViewControllers.
My application is separate in 3 VC : The login, the gallery and the settings (which contains the LogOut).
Here is my problem. When I log out, I want to create a new loginVC and clear all previous VC.
So I have my cleanMemory function which set all the UIImage to nil
func cleanMemory(request: Gallery.Request) { // Interactor
worker.cleanMemory(completionHandler: { (Value) in
self.interventions?.removeAll() // Interventions contains UIImages
self.interventionsSelected.removeAll() // InterventionsSelected contains UIImages
})
}
and then I delete the rests of the UIImage and the VC
func cleanMemory() {
interactor?.cleanMemory(request: Gallery.Request())
self.displayedInterventions.removeAll() // displayedInterventions contains UIImages
interactor = nil
router = nil
self.removeFromParentViewController()
self.navigationController?.popViewController(animated: true)
}
But when I create my new LoginVC.. my RAM didn't decrease.. And when I check the app memory, not a single VC was deleted.. And when I execute the loop Logout/Login 3 times, my app crash because I'm not well managing my RAM_
So where did I get wrong, and why ??
Thanks you for your answer.
EDIT: I was having 2 problems :
My completionHandler was keeping my VC alive
I was switching VC with .present, so that was keeping my VC in memory.
So you should change VC like that :
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let destinationVC = storyboard.instantiateViewController(withIdentifier: "LoginController")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = destinationVC
To remove viewController from memory you simply need to remove it from navigation stack. So when you call navigationController?.popViewController(animated: true) and back to previous view controller you already destroy that controller.
Then,
Here is my problem. When I log out, I want to create a new loginVC and clear all previous VC. So I have my cleanMemory function which set all the UIImage to nil
on logout it's good practice to stop all request but you don't need to do any changes to UI, because it takes some time and it doesn't need to "remove controller from memory". How to check if view controller completely removed from navigation stack? Simply write print statement in deinit func, compile code and go back from this view controller.
deinit {
print("ViewController deinit")
}
If this print works fine (you can see text in xcode console), you achieve the result - controller has been removed from navigation stack, but if there is no print result you probably forget to right manage your closures. For example
worker.cleanMemory(completionHandler: { (Value) in
...
})
this closure may hold your controller when your think that controller already deallocated and it means that your controller present somewhere in memory. To prevent these retain cycles you need to use [unowned self] of [weak self] (just google for this keywords, it's very easy to understand) like this:
// or you can use `[unowned self]`
worker.cleanMemory(completionHandler: { [weak self] (Value) in
guard let `self` = self else { return } // only for `weak` way
...
})
So, in this case there are nothing that can hold your controller alive after pop from navigation stack action or so.
These are simple rules that you should follow to write well managed code.

Referencing item's initialized in viewDidLoad safely

I ran into a bizarre bug earlier this week and wanted to follow up to see how to prevent the root cause of the issue.
Take the following code.
//*****************************
//MAINVIEWCONTROLLER CLASS CODE
//*****************************
//Some event happens that triggers me to want to load up TestViewController.
func showViewController(){
var testController = TestViewController()
testController.someMethod("Test1")
self.navigationController?.pushViewController(testController, animated: true)
}
//*****************************
//TESTVIEWCONTROLLER CLASS CODE
//*****************************
testView:TestView!
override func viewDidLoad(){
super.viewDidLoad()
testView = TestView()
...
}
func someMethod(someData:String){
testView.name = someData //AppCrashes here because testView might be nil.
...
}
So someMethod is getting fired before TestViewController has had the chance to go through and create the testView. I'm then getting a cannot unwrap an optional value because testView is nil and I'm accessing a property on it.
Whats strange is the application I'm running probably does this exact thing in 6 different places, and 5/6 are working perfectly fine, but 1/6 is now giving me this error. I'm guessing its because of the viewDidLoad not being guaranteed to fire immediately or complete before someMethod is executed, but why then is this not happening on all 6 of the use cases.
So my main questions are:
Why does this crash happen?
What is the best practice to avoid it.
Thanks! Thoughtful answers will get up-votes as always! Let me know if any more info would be helpful.
Basically never run code in the destination controller called from the source controller which involves UI elements. Create a property, set it in the source controller and assign the value to the UI element in viewDidLoad() of the destination controller, for example:
//*****************************
//MAINVIEWCONTROLLER CLASS CODE
//*****************************
//Some event happens that triggers me to want to load up TestViewController.
func showViewController(){
var testController = TestViewController()
testController.someData = "Test1"
self.navigationController?.pushViewController(testController, animated: true)
}
//*****************************
//TESTVIEWCONTROLLER CLASS CODE
//*****************************
testView:TestView!
var someData = ""
override func viewDidLoad(){
super.viewDidLoad()
testView = TestView()
testView.name = someData
...
}
viewDidLoad is called when the ViewController completes loading in preparation to be shown, e.g. when a segue takes place or when involved in a present.
As written your code shouldn't even compile since testView is optional, but you have two options. Use optionals (in which case the view may not get the information if not called after viewDidLoad, but it won't crash) or store the passed information and update your view in viewDidLoad or viewWillAppear.
Something you might want to be aware of is viewIfLoaded
You can force the viewDidLoad method with with:
var testViewController = TestViewController()
_ = testViewController.view
testViewController.someMethod("Test")
Initializing the ViewController doesn't automatically call viewDidLoad

Reference counting of UIViewController

I just experienced a strange behavior when I do the following steps,
In a view controller method, create a View controller instance (local instance).
Add the view as a subview to the view controller's view.
The view is displayed properly. The view has a button and when I click the button, it crashes with EXEC_BAD-ACCESS. After debugging a while, found that the view controller instance is release and button click event is not fired since the controller doesnot exist.
When I moved the declaration of the view controller to class level, it started working. I feel if a view controller's view is on the screen, shouldn't the view controller instance be retained.
Any thoughts?
Some code pointer.
class SomeViewController:UIViewController{
var workingVC:SomeVC?
func crashingMethod()
{
let vc:SomeVC = SomeVC(nibName:"SomeVC", bundle:NSBundle.mainBundle())
let delegate:AppDelegate = UIApplication.sharedApplication().delegate
let applWindow:UIWindow = delegate.window!
applWindow.addSubview(vc.view)
}
func workingMethod()
{
self.workingVC = SomeVC(nibName:"SomeVC", bundle:NSBundle.mainBundle())
let delegate:AppDelegate = UIApplication.sharedApplication().delegate
let applWindow:UIWindow = delegate.window!
applWindow.addSubview(self.workingVC!.view)
}
}
When you create object like this
let vc:SomeVC = SomeVC(nibName:"SomeVC", bundle:NSBundle.mainBundle())
object lifetime is equal to scope where it is created.
When scope ends, ARC release this object and all associated objects.
But, when you add it on class level, lifetime of this object equal to lifetime of owner(SomeViewController). So, it is released only when owner released.
Is it clear for you?
Also you can read about it in Memory Management Section in Apple Documentation

ios swift parse: methods with async results

When I go to a viewController I call within my viewDidAppear Method a function:
override func viewDidAppear(animated: Bool) {
getLessons()
}
This methods loads from parse.com a list of data I want to use in a pickerView.
The function itself:
func getLessons(){
var query = PFQuery(className:"Lesson")
query.orderByAscending("name")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
var name = object["name"] as String
self.languagePickerKeys.append(object.objectId)
self.languagePickerValues.append(name)
self.selectedLanguage.text = self.languagePickerValues.first // set the first lessons name into the text field
self.selectedLessonObjectId = self.languagePickerKeys.first // set the first objectId for the lesson
self.languagePicker?.reloadAllComponents()
}
} else {
// Log details of the failure
println("\(error.userInfo)")
}
}
println("getLessons done")
}
The thing is, that the textfield is empty, as the getLesson() gets the data async and the data is not available to the textfield.
I also tried to put the getLesson into the viewDidAppear method, but this doesn't help me, the textfield is empty anyway.
What can I do, to have the data from the getLessons() method ready and loaded its first value into my textfield when the view is shown to the user?
You certainly have to get the data from asyncTask before setting it to pickerView.
Here's the ViewController lifecycle after instantiation:
Preparation if being segued to.
Outlet setting
Appearing and Disappearing.
So, you have two options:
Load the data in previous ViewController and then perform the segue. You need to follow these steps for it.
a. Create a segue from previous ViewController to your ViewController.
b. Call the function when you want to go next ViewController which fetches the data, and the end (after getting the data) call performSegueWithIdentifier which will lead to your ViewController.
c. Set the data in prepareForSegue
let navigationController = segue.destinationViewController as UINavigationController
navigationController.data = yourData //you got from async call
Now when you reach your ViewController, you are sure that your data is present, and you can set it to your pickerView.
If you want to do it in the same ViewController: here's is the lifeCycle of ViewController:so you need to call your function in viewDidLoad, and always set your pickerView after completion of the async network call.
Make sure that you initiate all changes to the UI from the main thread e.g. like so:
dispatch_async(dispatch_get_main_queue(), {
selectedLanguage.text = languagePickerValues.first
self.languagePicker?.reloadAllComponents()
})
The problem is that findObjectsInBackgroundWithBlock is an asynchronous method, so even if you fire it in the ViewDidLoad you will never know when you will receive the response data and you can't be sure that the data will be ready by the time you view appear.
I think you have just 2 possibility:
The first one is to load the data in the previous view controller and then just pass the data that got ready to you view controller.
The second is to use a synchronous method (the findobject method maybe?) and put the call in a method that is fired BEFORE the view appear (like the viewWillAppear: method). But your view will stuck for a moment (I think) while the data is retreiving... However this second solution probably resolve your problem but using synchronous method to retrieve data from a slower data source is usually bad design solution.
D.

Resources