I have a Swift (2.2) iOS app (my first) with a couple of UITableViews. One of the views lists payments which are added / removed throughout the life of the program.
This all works fine 99% of the time, but a few times I have come across an issue where the UITableView all of a sudden becomes nil.
The IBOutlet must be hooked up correctly, or it would not work at all.
What could possibly be causing this when I am not assigning to the IBOutlet variable anywhere (just calling methods on it)?
Or (if I cannot find the cause), advice on best handling when it happens (if I need to recreate it, what about outlets, events, autolayout, etc.?)
#IBOutlet weak var paymentTableView: UITableView!
func handlePayment(payment: PaymentRecord) -> Void {
let existingPaymentIndex = payments.indexOf({ $0.payNo == payment.payNo })
if (existingPaymentIndex != nil) {
payments.removeAtIndex(existingPaymentIndex!)
}
if (self.paymentTableView == nil) { // Here is where I notice the issue
Log.error?.message("handlePayment: paymentTableView is nil!!")
return
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.paymentTableView.reloadData()
})
}
What is happening is that the tableview is being unloaded from memory when the view is disposed of. You likely have an asynchronous task completing, most usually from making a network call, that is returning and calling this function after the view was disposed. I've seen this happen many times when a view is trying to load a network resource and the user is able to switch to a different view before the network call's completion handler is called.
Related
I am working on an open source tutorial using MVVM, Coordinators and RxSwift. I am constructing all the viewcontrollers and models in the coordinator. Controller has a strong reference to viewmodel and when a viewmodel is set, I would like to perform some UI related actions(using property observer didSet). The problem I am facing is that didSet is called before viewDidLoad causing a crash.
Stripped down version of ViewController:
class MessageVC: UIViewController {
var viewModel: MessageViewModel! {
didSet {
manipulateUI() // crashes
}
}
override func viewDidLoad() {
super.viewDidLoad()
manipulateUI() // works fine if setup is correct in coordinator
}
Coordinator stripped down version:
extension AppCoordinator {
convenience init() {
let rootVC = MessageVC() // actual construction from storyboard
let messages = Message.getMessages()
rootVC.viewModel = MessageViewModel(withMessage: messages)
}
My concern is that even though calling manipulateUI in viewDidLoad is working for me currently, the app will crash if I forget to set the viewModel from my co-ordinator making me think that I am using a fragile architecture. I really like updating userinterface from didSet but it is called before viewDidLoad.
I know it is a simple problem but from architecture standpoint it seems fragile. Any suggestions, improvements and comments are appreciated a lot.
I wont say that cases like this can define wether you are dealing with fragile architecture or not because view controllers has their own life cycle which differs a lot from other objects life cycle. Anyway you can easily avoid crashes here using different approaches. For example :
Approach 1:
Put a guard statement at the very beginning of your manipulateUI function so this function wont manipulate UI until both view is loaded and model is set. Then call this function on viewDidLoad method and when viewModel is set:
func manipulateUI(){
guard let viewModel = self.viewModel , isViewLoaded else {
return
}
//continue manipulation here
}
Approach 2:
Since you are not sure wether view is loaded when you set the model and don't know if views are initialized yet, you can access the views as optional properties in manipulateUI function:
func manipulateUI(){
self.someLabel?.text = self.viewModel.someText
//continue manipulation here
}
Approach 3:
Since you are using RxSwift you can always register an observer for view controller's isViewLoaded property and set the data source after you are sure that view is loaded
Crash happens because at this point
rootVC.viewModel = MessageViewModel(withMessage: messages)
view controller is not initialized.
It won't work the way you're trying to accomplish, you have to call manipulateUI() inside viewDidLoad.
As far as I have worked in xamarin declaring global variables in view controller is very important or else it can be garbage collected when it comes to Binding and all concerned.
likewise for nsnotification we have to have a global reference of nsobject type and remove the nsobject when we don't need nsnotification.
there are no practical documentation available and making native iOS developers to get frustrated with xamarin.ios
Suggesting few things like this would be a good help for any ios developer who becomes xamarin.ios developer
In Every ViewControllers Write following Code:
public override void ViewDidDisappear (bool animated)
{
//Executed when we navigate to other view, moving this ViewController in Navigation stack
//1. UnSubscribe All Events Here (Note: These must be SubScribed back in ViewWillAppear)
btnLogin.TouchupInside -= OnLoginClicked
//2. Remove Any TapGuestures (Note: These must be added back in ViewWillAppear)
if (singleTapGuesture != null)
{
scrollView.RemoveGestureRecognizer(singleTapGuesture);
singleTapGuesture.Dispose();
singleTapGuesture = null;
}
//3. Remove any NSNotifications (Note: These must be added back in ViewWillAppear)
//4. Clear anything here, which again initializes in ViewWillAppear
if (ParentViewController == null) {
//This section will be executed when ViewController is Removed From Stack
//1. Clear everything here
//2. Clear all Lists or other such objects
//3. Call Following Method which will clear all UI Components
ReleaseDesignerOutlets ();
}
base.ViewDidDisappear (animated);
}
This will clear out all unwanted object when ViewController is Navigated or Removed from the stack.
For UITableViews, use WeakDataSource & WeakDelegate.
There can be many more based on following Links:
Ref 1
Ref 2
I am trying to update my UILabel to a String from my Parse database.
My problem is the label will not update my firstnameLabel when I first sign in. But it WILL update, when i sign in (nothing happens), push the stop button in Xcode and then launch it again (still logged in) and then it updates the label.
How can I do this faster??
Here is my code:
var currentUser = PFUser.currentUser()
if currentUser != nil {
var query = PFQuery(className:"_User")
query.getObjectInBackgroundWithId(currentUser.objectId) {
(bruger: PFObject!, error: NSError!) -> Void in
if error == nil && bruger != nil {
var firstName: String = bruger["firstname"] as String
self.usernameLabel.text = firstName
} else {
println("Error")
}
}
} else {
self.performSegueWithIdentifier("goto_login", sender: self)
}
Hope you can help me!
Rather than trying to load the user object again by the id, try just doing a fetch instead.
[currentUser fetchIfNeededInBackgroundWithBlock: ... ];
By trying to load the object, you might be getting a cached version.
I read somewhere that it could be because of the fact that the could was put inside the ViewDidLoad. I tried to put it outside of that and it worked!
It sounds like you put it in the ViewDidLoad method. You should put it in the ViewWillAppear method instead. Here's an example.
1) ViewDidLoad - Whenever I'm adding controls to a view that should appear together with the view, right away, I put it in the ViewDidLoad method. Basically this method is called whenever the view was loaded into memory. So for example, if my view is a form with 3 labels, I would add the labels here; the view will never exist without those forms.
2) ViewWillAppear: I use ViewWillAppear usually just to update the data on the form. So, for the example above, I would use this to actually load the data from my domain into the form. Creation of UIViews is fairly expensive, and you should avoid as much as possible doing that on the ViewWillAppear method, becuase when this gets called, it means that the iPhone is already ready to show the UIView to the user, and anything heavy you do here will impact performance in a very visible manner (like animations being delayed, etc).
3) ViewDidAppear: Finally, I use the ViewDidAppear to start off new threads to things that would take a long time to execute, like for example doing a webservice call to get extra data for the form above.The good thing is that because the view already exists and is being displayed to the user, you can show a nice "Waiting" message to the user while you get the data.
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.
I'm trying to add some error handling (to cope with loses of network connectivity when initializing my view models as well as elsewhere) by having it publish a message that is picked up by my view models that will then do a ChangePresentation with a PresentationHint that causes my presenter (derived from MvxTouchViewPresenter) to do this:
this.MasterNavigationController.PopToRootViewController(false);
This occasionally works, but a lot of the time it doesn't, getting stuck on whatever view it was currently on and I see the message Unbalanced calls to begin/end appearance transitions for <MyView: 0x...>. I believe this is because sometimes the message is getting thrown before the view that was loading has had time to finish loading (the actual loading of data is asynchronous and fires up on another thread - hence the problem).
So my question is, is there a way to synchronize this so that instead of immediately popping to the root, it will finish what it's doing, then pop to the root? Or is there some better way to handle this?
There's not quite enough code in your question to work out what's happening. There are lots of other questions on StackOverflow containing that error message - it may be worth looking through them to see if there is a nice solution to your issue. e.g. UINavigationController popToRootViewController, and then immediately push a new view
If you do want to detect 'finish loading' then on way to do this is to listen to the ViewDidAppear messages in the ViewControllers that are being shown. In mvx, this is easy to do as all ViewController's support a ViewDidAppearCalled event that you could easily hook up in your custom presenter:
private readonly Queue<Action> _pendingActions = new Queue<Action>();
private bool _isBusy;
public override void Show(Cirrious.MvvmCross.Touch.Views.IMvxTouchView view)
{
if (_isBusy)
{
_pendingActions.Enqueue(() => Show(view));
return;
}
_isBusy = true;
var eventSource = view as IMvxEventSourceViewController;
eventSource.ViewDidAppearCalled += OnViewAppeared;
base.Show(view);
}
private void OnViewAppeared(object sender, MvxValueEventArgs<bool> mvxValueEventArgs)
{
_isBusy = false;
var eventSource = sender as IMvxEventSourceViewController;
eventSource.ViewDidAppearCalled -= OnViewAppeared;
if (!_pendingActions.Any())
return;
var action = _pendingActions.Dequeue();
action();
}
public override void ChangePresentation(Cirrious.MvvmCross.ViewModels.MvxPresentationHint hint)
{
if (_isBusy)
{
_pendingActions.Enqueue(() => ChangePresentation(hint));
return;
}
base.ChangePresentation(hint);
}
Note: this code requires 3.0.13 or later to work (there was a bug for ViewDidAppear in some view controllers in earlier versions)
If you are using a simple UINavigationController, another way to achieve a similar effect is to use the Delegate on that controller - see https://developer.apple.com/library/ios/documentation/uikit/reference/UINavigationControllerDelegate_Protocol/Reference/Reference.html