memory management issues in xamarin.ios - ios

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

Related

Detect when UISplitViewController changes display mode

I'm trying to use a UISplitViewController where the secondary controller should expose a "close" function (via a button or button bar item) whenever the UISplitViewController is in side-by-side mode, but should hide the function at other times. I tried putting this in the secondary view controller:
override func viewWillAppear(_ animated: Bool) {
if splitViewController!.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
This correctly sets the visibility of the "close" function when the secondary view is first displayed, but if the UISplitViewController switches between expanded and collapsed (say, by rotating an iPhone 6s Plus), then this function is not called again (which makes sense, as the secondary controller remains visible). Consequently, the "close" function remains in its initial state--hidden or shown--even as the UISplitViewController changes mode.
How can I get the "close" function to hide or show in reaction to changes in the mode of the UISplitViewController?
There is the UIViewControllerShowDetailTargetDidChangeNotification notification for that:
// Sometimes view controllers that are using showViewController:sender and
// showDetailViewController:sender: will need to know when the split view
// controller environment above it has changed. This notification will be
// posted when that happens (for example, when a split view controller is
// collapsing or expanding). The NSNotification's object will be the view
// controller that caused the change.
UIKIT_EXTERN NSNotificationName const UIViewControllerShowDetailTargetDidChangeNotification NS_AVAILABLE_IOS(8_0);
Use as follows
- (void)viewDidLoad{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(showDetailTargetDidChange:) name:UIViewControllerShowDetailTargetDidChangeNotification object:self.splitViewController];
}
- (void)showDetailTargetDidChange:(NSNotification *)notification{
// changed from collapsed to expanded or vice versa
}
This Apple sample demonstrates how the table cell accessory changes from a disclosure indicator in portrait (denoting that a push will happen) to it being removed when changing to landscape split view:
https://developer.apple.com/library/archive/samplecode/AdaptivePhotos/Introduction/Intro.html
Note on iOS 13 beta, use addObserver with object nil because there currently is a bug they send the notification using the wrong object. They use their new UISplitViewControllerPanelImpl from the internal class cluster instead of the UISplitViewController object.
http://www.openradar.appspot.com/radar?id=4956722791710720
For future reference:
What about using the UISplitViewControllerDelegate??
It has a method called
splitViewController:willChangeToDisplayMode:
that should do exactly what you where looking for.
Documentation here
Okay, I found a simple solution. I was making a novice mistake. The trick is to override viewWillLayoutSubviews() instead of viewWillAppear(animated:). Then everything works as I want. It seems that viewWillLayoutSubviews() is called (sometimes more than once) every time the containing UISplitViewController changes its display mode, and that's exactly what I need to respond to. The only gotcha is that splitViewController might be nil on some of those calls, so it needs to be implemented like this:
override func viewWillAppear(_ animated: Bool) {
if let svc = splitViewController {
if svc.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
}
As part of my stumbling around to find a solution, I tried overriding traitCollectionDidChange(previousTraitCollection:). (I tried this because I wanted to react to device rotations.) At first I thought I was onto something, because this function also get called whenever the device rotates. Interestingly (and, frustratingly) I found that my view's splitViewController property was nil when this function is called. It seems odd that this should be so, since neither viewDidDisappear(animated:) nor viewWillAppear(animated:) is called when the UISplitViewController reconfigures itself. But why it should be nil is, I suppose, a question for another day.

Swift iOS, UITableView nil

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.

One class instantiation swift share between View Controllers

I'm having a bit of trouble getting this right. What i have is a NavigationController with several View Controllers. In the first View Controller i instantiate a new class, GPSTrackingManager:
var tracking = GPSTrackingManager()
Because i need GPS data and functions available in all view controllers after this UINavController. I'm calling this already in the first VC so the GPS can get a early fix. How can i pass this tracking object to the new view controllers? I tried several things in the prepareForSegue, but i'm out of ideas currently. If i put the above code in the new VC then i have the startUpdatingLocation() running 2 times. If you can point me in the right direction that would be greatly appreciated!
I think the most appropriate solution (without going too overboard, because there are certainly more thorough ways to go about this problem) is to separate the gps management into its own singleton class, and to subscribe to a trackerUpdated protocol for updates.
import Foundation
import placeWhereGPSTrackingManagerIs
protocol GPSManagerDelegate {
func didUpdateTracker(tracker: yourTrackerObject)
}
let _GPSManagerSharedInstance = GPSManager()
class GPSManager {
weak var delegate: GPSManagerDelegate?
let tracker: yourTrackerObject?
class var sharedInstance:GPSManager {
return _GPSManagerSharedInstance
}
init() {
tracker = GPSTrackingManager()
tracker.startUpdatingLocation()
}
func updateTracker(newInformation) // you'll want to pass whatever information your tracker needs here
{
tracker.trackerProperty = newInformation
self.delegate?.didUpdateTracker(tracker)
}
}
Swift singleton explanation
Using a dispatch_once singleton model in Swift
Now, whatever startUpdatingLocation does could call updateTracker in this class with the new information it needs to update the tracker. You could also migrate that logic to live inside of here (which I recommend doing in this case) so everything is contained in one class.
Any view controller interested in the information can subscribe to the didUpdateTracker protocol method, which will pass the tracker through to that view controller
import UIKit
class controllerOne: UIViewController, GPSManagerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
GPSManager.sharedInstance.delegate = self // set the GPSManager delegate
if let tracker = GPSManager.sharedInstance.tracker {
// if the tracker exists, we assign it to the "tracker" variable, and use as needed
// setup page with tracker
}
}
func didUpdateTracker(updatedTracker: yourTrackerObject) {
// protocol method that will be called from the gpsmanager everytime it updates
// do whatever updates to the page with the
}
}
As you can only have one GPSManager and its tracker property cannot be changed, it is always safe to ask the GPS manager for this tracker, which only calls startUpdatingLocation() on its initialization
You could also post notifications as #shesh nath mentioned, which is a perfectly valid way to broadcast changes if you prefer it to the delegation method. Either way you approach it, I would recommend separating the tracker from the view controller so that the view controllers are not the ones in charge of managing the trackers state.
There are several ways to achieve it like
1) using Key-Value observer
2) Notification
Example of Notification is on your first viewController post notification and pass your data to this notification.
NSDictionary *data;
[[NSNotificationCenter defaultCenter] postNotificationName:#"updateBeaconStateOnDetailScreen" object:data];
and on left-over all viewcontroller register listener for this notification on viewdidload example.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateBeaconState:)name:#"updateBeaconStateOnDetailScreen" object:nil];
and your method which will be called by this notification is
-(void) updateBeaconState:(NSNotification *) notification
{ //notification raised from BeaconList with updated Beacon State
NSDictionary *dictUpdatedBeacon=[notification valueForKey:#"object"];
}

Meaning of exclamation mark in iOS Designer on Xamarin Studio

I'm working with the iOS Designer and I have a storyboard with different views on it (navigation controller, ...). On nearly each view there is a red exclamation mark on the right side at the bottom. I get errors like
System.NullReferenceException
Object reference not set to an instance of an object
If I go to the corresponding code line:
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations ()
{
return TopViewController.GetSupportedInterfaceOrientations();
}
This is part of my CustomNavigationController. If I set the class name on the iOS Designer I get the error. What do I have to add here? The constructor looks like the following:
public CustomNavigationController (IntPtr handle) : base (handle)
{
}
Do I have to instantiate it in the AppDelegate?
On another view controller I get a similar Exception (with a different stack) and it points me to this line:
this.NavigationController.NavigationBar.TintColor = UIColor.FromRGB (21, 66, 139);
The app seems to work fine on simulator and on device though.
How do I resolve this issue?
EDIT:
I now removed the code in my view controller:
this.NavigationController.NavigationBar.TintColor = UIColor.FromRGB (21, 66, 139);
As replacement I moved this code to the Application Delegate FinishedLaunching():
UINavigationBar.Appearance.TintColor = UIColor.FromRGB (21, 66, 139);
The result is that now I don't have the exclamation mark for this view controller and the color is as desired. But the preview in the iOS Designer doesn't show me the correct color anymore.
If I comment out GetSupportedInterfaceOrientations() in my CustomNavigationController than the other exclamation mark has been removed. I thought I need this function for defining the orientation of each view controller. A quick look showed that it seems to work.
There are restriction on what the design time can support. You should replace any dynamic data, for example data loaded from a service, with dummy data when on design mode. The same applies to runtime objects like TopViewController which is only available at runtime.
if (Site != null && Site.DesignMode)
{
// use dummy data for designer surface
}
else
{
// use live data
}
This document explains it in more detail:
http://developer.xamarin.com/guides/ios/user_interface/designer/ios_designable_controls_overview/

Handling Unbalanced calls to begin / end appearance when popping controller

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

Resources