Consider we have a custom UIViewContoller.
Now we have to make some clean up on UIViewController unload. For example, if we use UIWebView as a subview in our view controller, it's recommended to set its delegate to null and call StopLoading() method on unload.
Some sources say it's not recommended to make clean up overriding Dispose() method, because it concerns only managed object lifecycle.
Other sources say it's not recommended to use ViewDidDissappear() for these purposes because it can be called twice.
How to handle it right?
ViewDidDisappear() and ViewWillDisappear() will not be called multiple times; at least not without a balanced call to ViewDidAppear() and ViewWillAppear(). If you see more calls to the disappear methods than to the appear methods, you have a bug in your code. If you want to make sure, your cleanup happens only once (for sanity), but isn't the solution something as simple as this:
bool myCleanupIsDone = false;
public override void ViewDidDisappear()
{
if(myCleanupIsDone)
{
return;
}
myCleanupIsDone = true;
CleanUpWhateverNeedsToBeCleanedUp();
}
You should almost never need to override Dispose() unless you are dealing with unmanaged resources. Xamarin.iOS does that a lot internally but for your code and in your case it is not relevant.
Normally you would create your required objects in ViewWillAppear() and clean them up in the disappear methods. That way you would not need to check if something has already been cleaned up.
ViewWillDisappear and ViewDidDisappear can actually be called multiple times, on iPhone for example every time when a new controller is presented or pushed. But I think it's still the right place to do all the necessary cleanups.
Krumelur's proposal would end in a null delegate of your UIWebView once your controller has been disappeared for the first time – if the user comes back, it would probably crash.
So instead of setting a flag you could check, if the controller you want to clean up is beeing popped or dismissed – if that's the case, you can safely do all the work.
Here's some code I found in one of my projects (seems I've been through this before ;)):
public override void ViewDidDisappear(bool animated)
{
if ((NavigationController == null && IsMovingFromParentViewController) ||
(ParentViewController != null && ParentViewController.IsBeingDismissed))
{
CleanUpAfterDisappearing();
}
base.ViewDidDisappear(animated);
}
Maybe you can just cleanup resources when the operating system instructs your app to do so via overriding the DidReceiveMemoryWarning() method in your custom view controllers: http://iosapi.xamarin.com/index.aspx?link=M%3AMonoTouch.UIKit.UIViewController.DidReceiveMemoryWarning
Related
I have a UITableView in my ViewController. To populate it, I have to make an async request that may take up to a second to complete. Where should I put it?
When I tried to make ViewDidLoad async and make a call from there, ViewWillLayoutSubviews throws an error, because it tries to position some elements that weren't assigned yet - due to the fact that not all code from ViewDidLoad was executed yet (I think).
Before awaiting anything in ViewDidLoad you need to setup all your view logic. Otherwise your view initialization will not be finished when ViewDidLoad method returns. That could be a potential cause for ViewWillLayoutSubviews to fail. If it still fails, use a try/catch to make sure your service is working:
public override async void ViewDidLoad()
{
base.ViewDidLoad();
// setup all the view elements here (before the async call)
try
{
var results = await MakeYourAsyncRequest();
InvokeOnMainThread(() =>
{
_tableView.Source = ...; // do something with the results
_tableView.ReloadData();
});
}
catch(Exception ex)
{
// do something with the exception
}
}
Try putting the tableView.ReloadData(); method inside
dispatch_async(dispatch_get_main_queue(), ^{} this might solve your issue.
-:As a general rule, you should try to make sure that all of your UI interaction happens on the main thread. And your data fetching task will work in background. It looked like you were calling reload Data from your
background thread, which seems risky.
Depending on the data I would put the call in the AppDelegate. When the app launches the data should be fetched and saved.
When your UITableview appears it will already have the data ready or maybe an error message since you already know the result of the fetch.
The data may change thats why I would also put the fetch call in viewWillAppear() of your ViewController with the UITableview.
ViewDidLoad() is a method that gets called only once. Also it is called as the first method of the ViewController lifecycle.
It would be good if you read a bit about it VC lifecycle.
You can help yourself by trying it in code with printf("method name").
I'm new to swift and ios programming in general. I'm trying to display a modal view when my app first loads which it does. The problem I'm running into is that my modal keeps appearing over and over and over. Not sure where I'm going wrong.
BONUS QUESTION: Ultimately I'd like this to only happen the first time the user opens the app.
class ViewController: UIViewController {
var introModalDidDisplay = 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)
showIntroModal()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func showIntroModal() {
if (!introModalDidDisplay) {
println(introModalDidDisplay)
introModalDidDisplay = true
let intro = self.storyboard?.instantiateViewControllerWithIdentifier("introModal") as IntroModalViewController
intro.modalPresentationStyle = UIModalPresentationStyle.FormSheet
self.presentViewController(intro, animated: true, completion: nil)
}
}
}
Found it. My "intro" class was extending ViewController rather than UIViewController...apparently that's bad. Thanks for the help! Sorry for the wild goose chase.
When you close the modal view you show your ViewController view again, firing viewDidAppear once more and entering an infinite loop of showing your modal view, since the first view is always "appearing"
I'd suggest doing this in viewDidLoad, as the view is supposed to load only once. Try and experiment with these events and see when they are fired.
As for firing only once I'd suggest setting a flag in localStorage (plist) indicating whether it's the first time the user opens the app or not. For example set a flag in the first view's viewDidLoad and if that flag is false show your modal view and set the flag to true.
Here's a question about writing in plists in Swift: Save Data to .plist File in Swift
A couple of observations:
Are you saying that you're seeing this appear again and again while you're using the app? That would suggest that you have multiple instances of this view controller instantiated. For example, you might be doing a segue back to this view controller (which will create new instance) rather than unwinding/popping/dismissing back to it (which will return to the previous instance).
I'd suggest you have a breakpoint or logging statement in viewDidLoad and confirm that you see this once and only once. If you're seeing it multiple times, that means that you have some circular reference amongst your storyboard scenes (and, BTW, you are abandoning memory, a type of leak).
To handle this only presenting itself once between uses of the app, you need to save this introModalDidDisplay in some form of persistent storage. Often NSUserDefaults is used for this. For example, define introModalDidDisplay to look up the status in the standard user defaults:
var introModalDidDisplay = NSUserDefaults.standardUserDefaults().boolForKey("introModalDidDisplay")
Then your showIntroModal can update this setting in the user defaults:
func showIntroModal() {
if !introModalDidDisplay {
introModalDidDisplay = true
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "introModalDidDisplay")
NSUserDefaults.standardUserDefaults().synchronize()
let intro = self.storyboard?.instantiateViewControllerWithIdentifier("introModal") as IntroModalViewController
intro.modalPresentationStyle = UIModalPresentationStyle.FormSheet
self.presentViewController(intro, animated: true, completion: nil)
}
}
Clearly, you can use whatever persistent storage technique you want (plist, archive, user defaults, Core Data, SQLite), but the idea is the same: Retrieve the status from persistent storage and once the intro screen has been presented, update that persistent storage accordingly.
By the way, by looking this up in persistent storage, we also fix the symptom of the problem I discussed in point #1. But you really want to fix the root cause of that first point, too, because otherwise you'll be leaking memory (if, of course, you're really instantiating multiple copies of the ViewController class).
By the way, looking ahead to the future, rather than storing just a boolean, I might suggest storing a version number identifier, too. That way, when you release version 2.0 of the app, you'll be able to decide whether the v1.0 users might see the updated intro screen again (or perhaps a custom one that focuses on what's new).
I'm using MvvmCross 3.0.12 in an iPad project. Currently, I'm getting a NPE in MvxViewControllerExtensionMethods.LoadViewModel, because touchView.Request is null. This is only happening in a view controller that is inheriting from MvxTableViewController; view controllers inheriting from MvxViewController load and display just fine.
I've set breakpoint in MvxTouchViewsContainer.CreateView -> MyTableViewController.Ctor -> MvxBaseTouchViewPresenter.Show -> MyTableViewController.LoadView; which have all referenced the same instance of the class. Then when I hit a breakpoint in ViewDidLoad, its a new instance and all of the properties including Request are null.
Is this a bug with Xamarin.iOS or am I doing something wrong?
This sometimes happens for view controllers like table, tab bar and collection.
It's caused, I think, by something in the Objective C base class init referencing the View - and thus triggering ViewDidLoad before the C# construction is fully complete. This is a bit like what can happen in 'pure' c# if a base class constructor references a virtual method.
To check this is occurring for your app, put 2 breakpoints in your app - one in the ViewController constructor and one in ViewDidLoad - if ViewDidLoad is fired first, then you know this is the case.
The only way around this I've found is to code around this by triggering a second ViewDidLoad call in the constructor.
public FirstView()
{
// need this additional call to ViewDidLoad because UIkit creates the view before the C# hierarchy has been constructed
ViewDidLoad();
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
if (ViewModel == null)
return;
// ...
If it helps:
I believe I've talked about this in at least one N+1 - maybe N=25 - see https://github.com/MvvmCross/NPlus1DaysOfMvvmCross/blob/master/N-25-Tabbed/Tabbed.Touch/Views/FirstView.cs and http://mvvmcross.wordpress.com
The same type of thing happens in pure objective C code - eg see initialize UITableViewController with [super initWithStyle:UITableViewStyleGrouped]
I am attempting to structure my application using the MVVM pattern. Therefor I have ViewModels that raise events when data changes, and the UI is expected to react to those events and update the visible UI controls.
I have a derived UITableViewCell that gets initialized with a certain ViewModel every time a new cell is created or dequeued (very similar to miguel's example here). One main difference being part of the initializing relies on subscribing to an event of the ViewModel. This creates a reference from the long lived ViewModel to this specific cell, holding it in memory for the lifetime of the ViewModel. When the cell is re-used, the old subscription is cleaned up and a new one created to the new ViewModel, that works fine.
However the problem is there doesn't seem to be any opportunity to clean up the last subscription once the cell is completely finished, which means it is held in memory for the lifetime of the ViewModel (much longer than I want it to be).
'Completely finished' depends on the VC hierarchy, but in this case the derived DialogViewController that contains the TableView with custom cells has been popped from the UINavigationController stack and has been disposed.
willMoveToSuperview is never called (I was hoping it would be with 'null' being passed in).
removeFromSuperview is never called.
Dispose on each cell is never called.
Disposing of the UITableViewController doesn't dispose each cell.
Disposing of the TableView within the controller doesn't even dispose each cell.
The only way I can manually dispose each cell (and hence clean up subscriptions) is by enumerating the cells manually myself in each of my derived UIViewControllers, something I want to avoid.
Has anyone has similar troubles like this? I cant be the first using the MVVM pattern with UITableViewCells.
Is this a bug with the Dispose pattern in the base MonoTouch UIKit wrappers?
EDIT:
Here is cut-down version of one of the custom UITableViewCells. Note I'm taking a pragmatic approach where I explicitly subscribe to events of properties I know may change, not a full MVVM bind every property to the UI. So my binding code consists of just standard event subscriptions:
public class MyCustomCell : UITableViewCell
{
private InvoiceViewModel currentViewModel;
private readonly UILabel label1;
private readonly UILabel label2;
public MyCustomCell(NSString reuseId)
: base(UITableViewCellStyle.Default, reuseId)
{
Accessory = UITableViewCellAccessory.DisclosureIndicator;
SelectedBackgroundView = new UIView()
{
BackgroundColor = UIColor.FromRGB(235,235,235),
};
label1 = new UILabel();
ContentView.Add(label1);
// The rest of the UI setup...
}
public void Update(MyViewModel viewModel)
{
if ( currentViewModel == viewModel )
return;
if ( currentViewModel != null )
{
// Cleanup old bindings.
currentViewModel.UnacknowledgedRemindersChanged -= HandleNotificationsChanged;
}
currentViewModel = viewModel;
if ( viewModel != null )
{
viewModel.UnacknowledgedRemindersChanged += HandleNotificationsChanged;
label1.Text = viewModel.SomeProperty;
// Update the rest of the UI with the current view model.
}
}
private void HandleNotificationsChanged()
{
// Event can fire on background thread.
BeginInvokeOnMainThread(() =>
{
// Relevant UI updates go here.
});
}
protected override void Dispose(bool disposing)
{
// Unsubscribes from ViewModel events.
Update(null);
base.Dispose(disposing);
}
}
And my derived MT.D element class has a 1:1 element:viewmodel, so the GetCell method looks like this:
public override UITableViewCell GetCell (UITableView tv)
{
var cell = (MyCustomCell) tv.DequeueReusableCell(key);
if (cell == null)
cell = new MyCustomCell(key);
cell.Update(viewModel);
return cell;
}
You're definitely not the first to do Mvvm table cells with MonoTouch.
I've blogged about it recently at http://slodge.blogspot.co.uk/2013/01/uitableviewcell-using-xib-editor.html
Before that there have been projects at NDC (search for "Flights of Norway") and there was a long running Mvvm project built on top of MonoTouch.Dialog.
In MvvmCross apps, we use tables bound to ObservableCollections and to other IList classes a lot.
Within these we generally don't hit many problems with left-living references, but that's because we generally don't encourage people to use long-living ViewModels - we try to create a new instance of ViewModel data to go with each View. However, I do understand that that may not be suitable for all appications.
When an Mvx user finds themselves with this type of problem, then some of the approaches we've tried are:
in iOS5 we did use the ViewDidUnload method to clean up bindings - but obviously that is now gone in iOS6.
depending on the UI presentation style (modal, splitview, navigationcontroller, popup, etc) we have tried manually detecting when views are 'popped' and using this to clear up bindings
again depending on the UI presentation style, we have tried using the ViewDidAppear, ViewDidDisappear events to add and tidy up the bindings
for all UIKit classes with data-binding we've always used Dispose as an extra place to try to clear up bindings
we've looked at using WeakReferences (especially from the ViewModel to the View) in order to get around the issues where the UIKit objects are owned by both iOS/ObjC and by MonoTouch/.Net - these problems are particularly hard to debug.
this weak reference code is likely to be one the key changes in the next release.
An example discussion about this (in Droid rather than in Touch) is on https://github.com/slodge/MvvmCross/issues/17
I'm sorry I can't offer you any specific advice at this moment. If you post some more example code about how you are creating and storing your bindings, I might be able to help more - but I can't really visualise what bindings you are creating right now.
I've got a few more links I'll add to this answer later - on mobile at present - too hard to add them here!
Update - for some more explanation on the WeakReference ideas, this is where we're heading now in v3 for MvvmCross - https://github.com/slodge/MvvmCross/tree/vNextDialog/Cirrious/Cirrious.MvvmCross.Binding/WeakSubscription - basically the idea is to use disposable weakreference event subscriptions - which won't keep the UIKit objects in RAM. It's not properly tested code yet. When it is, then I'll blog and talk about it more fully!
The application I'm writing needs to support iOS5+. Recently, Apple obsoleted ViewDidUnload as we're told there is no significant memory gain in releasing views on memory warning.
In my application, I have a UIViewController that manages a very heavy UIWebView.
This view controller is presented modally and, as a result, often being created and dismissed.
By using Instruments, I found out that the memory taken by UIWebView is not being freed immediately after its controller is dismissed.
I assumed that the controller would eventually get collected by Mono GC, and it would call Dispose on the controller, as well as on its view, which would dispose UIWebView and free underlying native object.
I can't test if this is the case: unfortunately after presenting and dismissing the controller for about ten times, I get a memory warning and the app crashes the next second. I'm not sure if Mono GC gets a chance to run at all.
So what I did was adding GC.Collect call right after the controller has been dismissed.
I also had to add ReleaseDesignerOutlets in ViewDidDisappear.
This seems to free UIWebView.
Update: I already found out that ReleaseDesignerOutlets call in ViewDidDisappear was obviously releasing the web view, but there was no benefit to GC call. In fact, GC never collected my controller because a button click handler was keeping the whole controller alive.
Now, I feel completely lost in some kind of Cargo memory management.
Is it reasonably to force garbage collection in my case?
Why do I have to call ReleaseDesignerOutlets? Surely, if there are no references to the “dead” controller, its views should be considered eligible for collection as well?
From Instruments heapshot diff, it looks like the views created from code “hold on” to the controller as well. Do I have to dispose them? Nullify them?
Do I need to manually call Dispose on the controller I just dismissed?
Do I need to include ReleaseDesignerOutlets call in Dispose method of my controller?
Do I need to null out references to child views in my custom UIView subclasses on Dispose?
You should just call Dispose() on your controller when it is dismissed.
So something like:
private YourModalController modalController;
//When your button is clicked
partial void YourButtonClick() {
modalController = new YourModalController();
PresentViewController(modalController, true, delegate {
modalController.Dispose();
modalController = null;
});
}
In YourModalController, make sure you have:
public override void Dispose(bool disposing) {
ReleaseDesignerOutlets();
base.Dispose(disposing);
}
You don't necessarily have to worry about ViewDidUnload in this case, since this controller is disposed when dismissed.
Prior to iOS 6:
ViewDidUnload was called in a low memory warning for the app
on controllers that are still in memory, but not actively on the screen such as down the stack in a UINavigationController
On this event, you should dispose any views you have C# references to and set them to null
for iOS 6 this doesn't happen any more
Likewise if you have this:
private UIButton buttonIMadeFromCode;
You should check for null, dispose it, and set it to null in Dispose() and ViewDidUnload() (but only mess with ViewDidUnload if you are targeting less than iOS 6).
First: memory management in MonoTouch is a very complex topic, because MonoTouch (which is garbage collected) has to co-exist with ObjectiveC (which is reference counted).
As you have found out by now it is easy to run into cycles, and when these cross the MonoTouch/ObjectiveC boundary, the GC is not able to figure out exactly what's going on and free the entire cycle.
If you're interested in a more in-depth explanation, check this thread out.