I have a UIPageViewController in which I've implemented a caching mechanism. Practically I have a pool of view controllers that I try to reuse as much as possible when the next or previous view controller is requested in the UIPageViewControllerDataSource. So when the data source requests the previous or the next page I first check if that page has already been shown and return the appropriate view controller.
It all works fine with the default swipe gesture, and the internal method to change page. Now I need to disable the swipe gesture, so I'm not setting the data source, and instead, I have two buttons to go to the previous and next page, and using setViewControllers(_:direction:animated:completion:) to change page programmatically. It works fine when going always in the same direction, but as soon as I change it, and so I'm using a cached view controller, it shows a blank page.
The code is exactly the same as before, if not for setting the view controller programmatically. I've tried both with or without animation and the result is the same and if I remove caching it also works well. My question is, am I forgetting something? Is the UIPageViewController doing something internally that I'm not doing when the data source is assigned?
EDIT
Here's a simplified version of the code. It's a Xamarin Project, but I suppose it's understandable
public class MyPageViewController : AbstractPageViewController
{
const int CacheCapacity = 5;
public int initialItemId { get; set; }
public List<int> ItemIds { get; set; }
readonly List<ChildViewController> viewControllerCache = new List<ChildViewController>(CacheCapacity + 1);
public override void ViewDidLoad()
{
base.ViewDidLoad();
DataSource = null;
var vc = GetDocumentViewController(initialItemId);
SetViewControllers(new[] { vc }, UIPageViewControllerNavigationDirection.Forward, false, null);
var nextButtonItem = new UIBarButtonItem
{
Enabled = true
};
var previousButtonItem = new UIBarButtonItem
{
Enabled = true
};
nextButtonItem.Clicked += NextDocumentButton_Clicked;
previousButtonItem.Clicked += PreviousDocumentButton_Clicked;
var rightButtons = new UIBarButtonItem[2];
rightButtons[0] = nextButtonItem;
rightButtons[1] = previousButtonItem;
NavigationItem.SetRightBarButtonItems(rightButtons, false);
}
private void PreviousDocumentButton_Clicked(object sender, EventArgs e)
{
var referenceVc = (ChildViewController)ViewControllers.FirstOrDefault();
var referenceId = referenceVc.ItemId;
var index = ItemIds.FindIndex(dp => dp.Id == referenceId);
if (index < 1)
return;
var previousDocumentId = ItemIds[index - 1];
var vc = GetDocumentViewController(previousDocumentId);
SetViewControllers(new[] { vc }, UIPageViewControllerNavigationDirection.Reverse, false, null);
}
private void NextDocumentButton_Clicked(object sender, EventArgs e)
{
var referenceVc = (ChildViewController)ViewControllers.FirstOrDefault();
var referenceId = referenceVc.ItemId;
var index = ItemIds.FindIndex(dp => dp.Id == referenceId);
if (index < 0 || index >= ItemIds.Count)
return;
var nextDocumentId = ItemIds[index + 1];
var vc = GetDocumentViewController(nextDocumentId);
SetViewControllers(new[] { vc }, UIPageViewControllerNavigationDirection.Forward, false, null);
}
ChildViewController GetDocumentViewController(int ItemId)
{
var cachedViewController = viewControllerCache.FirstOrDefault(dvc => dvc.ItemId == ItemId);
if (cachedViewController != null)
return cachedViewController;
var vc = new ChildViewController();
vc.SetData(itemId);
viewControllerCache.Add(vc);
if (viewControllerCache.Count > CacheCapacity)
{
viewControllerCache[0].RecycleIfNeeded();
viewControllerCache.RemoveAt(0);
}
return vc;
}
}
It turned out that it was due to a recycling mechanism that was implemented in my code.
In particular, there's a forced recycling of the content of the view controller on ViewDidDisappear. I've disabled it in this case and then it works as it should. This also means that when the UIPageViewController has a data source, and is managing the child view controller directly, ViewDidDisappear is not called when changing page through swiping.
Related
I have created a tutorial window in storyboard with two views, one to hold show the tutorial the other used as a template for each page of content.
Some elements are coded programmatically on the ViewDidLoad event.
The PageViewController is working 100% as required, it shows the three pages of content and allows swiping backwards and forwards without issue.
I've added a UIPageControl programmatically to the main ViewController but for the life of me cannot update its CurrentPage value correctly. Accessing the datasources PageIndex value gives me odd results when swiping back and forth.
Is there a reliable way to know exactly which page is been displayed ?
Or to know which direction the page transition moved, this way I can manually update? Not entirely sure how UIPageViewControllerNavigationDirection is accessed from the pagecontrollers 'DidFinishAnimating' event.
My main view controller code is as follows:
using Foundation;
using System;
using UIKit;
namespace Performance
{
partial class VCOnboardHome : UIViewController
{
const int pageCount = 3;
public UIPageViewController pvcOnboarding{ get; set; }
private OnboardingDataSource onboardDataSource;
UIStoryboard board;
public UIPageControl pgControlIndicator;
public VCOnboardHome (IntPtr handle) : base (handle)
{
}
/// <summary>
/// ViewDidLoad event method
/// </summary>
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
board = UIStoryboard.FromName ("Main", null);
// Programmatically create a PageView controller
pvcOnboarding = new UIPageViewController (UIPageViewControllerTransitionStyle.Scroll,
UIPageViewControllerNavigationOrientation.Horizontal,
UIPageViewControllerSpineLocation.None, 20f);
// PageView Controller datasource
var views = CreateViews ();
onboardDataSource = new OnboardingDataSource (views);
pvcOnboarding.DataSource = onboardDataSource;
pvcOnboarding.SetViewControllers (new UIViewController[] { views [0] },
UIPageViewControllerNavigationDirection.Forward,
false, null);
// Set PageView size
pvcOnboarding.View.Frame = View.Bounds;
// Add the page view control to this view controller
Add (pvcOnboarding.View);
// Create Page Control
var frame = UIScreen.MainScreen.Bounds;
pgControlIndicator = new UIPageControl ();
pgControlIndicator.Frame = new CoreGraphics.CGRect (20f, frame.Height - 60f, frame.Width - 40f, 40f);
pgControlIndicator.Pages = pageCount;
pgControlIndicator.UserInteractionEnabled = false;
Add (pgControlIndicator);
// Update the Page Control to indicate the current page we are showing.
// Only do this if the full page transition happened and not a partial page turn
pvcOnboarding.DidFinishAnimating += (sender, e) => {
foreach(UIViewController u in e.PreviousViewControllers){
// TODO - Not needed, remove once page control working
// u = the previous viewcontroller
}
if(e.Finished && e.Completed){
// Page transition completed
// Update Page Control here
}else{
// Incomplete page transition
}
};
}
// Content for each page
VCOnboardContentNew[] CreateViews ()
{
var pageData = new [] {
new ContentOnBoardData {
headerLblText = #"Page 1",
bodyContentText = #"Page 1 body text blah blah blah blah",
buttonText = #"Ok, next no. 1",
pageImage = UIImage.FromBundle("ios_images_v2/onboarding/icon-qr-code.png"),
currentPage = 1,
totalPages = 3
},
new ContentOnBoardData {
headerLblText = #"Page 2",
bodyContentText = #"Page 2 body text blah blah blah blah",
buttonText = #"Ok, next no. 2",
pageImage = UIImage.FromBundle("ios_images_v2/onboarding/icon-id-check.png"),
currentPage = 2,
totalPages = 3
},
new ContentOnBoardData {
headerLblText = #"Page 3",
bodyContentText = #"Page 3 body text blah blah blah blah",
buttonText = #"Ok, got it",
pageImage = null,
currentPage = 3,
totalPages = 3
}
};
var views = new VCOnboardContentNew[pageData.Length];
for (int i = 0; i < pageCount; i++) {
int pageIndex = i;
views [i] = (VCOnboardContentNew)board.InstantiateViewController ("sbid_onboardcontent");
views [i].PageIndex = pageIndex;
views [i].HeaderText = pageData [i].headerLblText;
views [i].ContentText = pageData [i].bodyContentText;
views [i].PageImage = pageData [i].pageImage;
views [i].CurrentPage = pageData [i].currentPage;
views [i].TotalPages = pageCount;
views [i].ButtonText = pageData [i].buttonText;
views [i].ButtonClicked += (s, e) => {
DismissViewController (true, null);
};
}
return views;
}
}
/// <summary>
/// Onboarding data source.
/// </summary>
class OnboardingDataSource : UIPageViewControllerDataSource
{
readonly VCOnboardContentNew[] _views;
public OnboardingDataSource (VCOnboardContentNew[] views)
{
_views = views;
}
/// <summary>
/// Gets the previous view controller.
/// </summary>
/// <returns>The previous view controller.</returns>
/// <param name="pageViewController">Page view controller.</param>
/// <param name="referenceViewController">Reference view controller.</param>
public override UIViewController GetPreviousViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
int index = ((VCOnboardContentNew)referenceViewController).PageIndex;
bool controlCheck = (index <= 0);
UIViewController vcToReturn = controlCheck ? null : (_views [index - 1]);
return vcToReturn;
}
/// <summary>
/// Gets the next view controller.
/// </summary>
/// <returns>The next view controller.</returns>
/// <param name="pageViewController">Page view controller.</param>
/// <param name="referenceViewController">Reference view controller.</param>
public override UIViewController GetNextViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
int index = ((VCOnboardContentNew)referenceViewController).PageIndex;
bool controlCheck = index + 1 >= _views.Length;
UIViewController vcToReturn = controlCheck ? null : _views [index + 1];
return vcToReturn;
}
}
/// <summary>
/// Content onboard data.
/// </summary>
struct ContentOnBoardData
{
public string headerLblText;
public string bodyContentText;
public string buttonText;
public UIImage pageImage;
public int currentPage;
public int totalPages;
}
}
My Page content code view controller is as follows:
using System;
using UIKit;
namespace Performance
{
/// <summary>
/// Class: VCOnboardContentNew
/// </summary>
partial class VCOnboardContentNew : UIViewController
{
public EventHandler ButtonClicked;
public int PageIndex { get; set; }
public string HeaderText{ get; set; }
public UIImage PageImage{ get; set; }
public int CurrentPage{ get; set; }
public int TotalPages{ get; set; }
public string ContentText{ get; set; }
public string ButtonText{ get; set; }
public VCOnboardContentNew (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Set text for the main title
lblTest.Font = UIFont.FromName("FreightDispLight", 26f);
lblTest.Text = HeaderText;
// Set text for body content
lblContentBodyText.Font = UIFont.FromName("FreightDispLight", 14f);
lblContentBodyText.Text = ContentText;
pgCtrlWhichPageWeOn.Hidden = true;
if(PageImage != null){
pageContentImage.Hidden = false;
pageContentImage.Image = PageImage;
}else{
pageContentImage.Hidden = true;
}
btnCallToAction.SetTitle (ButtonText, UIControlState.Normal);
btnCallToAction.TouchUpInside += (object sender, EventArgs e) => {
if (ButtonClicked != null) {
ButtonClicked.Invoke (this, null);
}
};
}
}
}
When the transition style is set to scroll, the page view controller seems to:
As soon as one transition finishes, it immediately calls for the next (or previous) View Controller (with GetNextViewController), even though the user might not have started the next swipe; and
It keeps the view controller that was transitioned from, and assumes that it is the previous VC, so it doesn't call for the previous VC (with GetPreviousViewController) if the user swipes back.
This makes it pretty difficult to keep track of which VC is actually currently showing pretty difficult. If found (see this answer) that I had to use both the WillTransition event and the DidFinishAnimating event. I'm not familiar with Xamarin and C#, so forgive me if this syntax is way off, but I think something like this:
pvcOnboarding.WillTransition += (sender, e) => {
nextVCIndex = ((VCOnboardContentNew)e.PendingViewControllers[0]).PageIndex
}
pvcOnboarding.DidFinishAnimating += (sender, e) => {
if(e.Finished && e.Completed){
// Page transition completed
currentVCIndex = nextVCIndex
// Update Page Control here
}else{
// Incomplete page transition
}
};
You'll need to add currentVCIndex and nextVCIndex as class level variables.
Old question but was recently struggling with the same issue. Finally found a nice solution.
Provide your UIPageViewController with:
public static int pageIndex = 0; // or whatever start index
Then for each of your UIViewControllers to be loaded override ViewDidAppear:
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
MyCystomPageViewController.pageIndex = 1; // the index of this viewcontroller
}
This works since ViewDidAppear gets called when the view is added as subview. And thus will be called everytime you swipe.
I'm trying to implement a Snapchat like feature where the views are navigated by the user swiping left or right. I'm using a UIPageViewController for this.
There is a problem though when I slide onto a UIImagePickerController view. The camera displays properly as the user is sliding onto that view, however when the user lets go of the screen and the view locks into place the camera fully blurs almost as if a fog layer is applied to it and then refocuses back to normal.
The weird thing is this doesn't occur if the transition style is "Page Curl". It only happens on "Scroll".
How can I stop this behaviour?
4 View Controllers in storyboard - Master, PageView, Home and Create (code for master and create below)
Thanks.
HeroMasterViewController class:
partial class HeroMasterViewController : ViewControllerBase IUIPageViewControllerDataSource
{
private UIViewController[] _heroViewControllers;
private UIPageViewController _heroPageViewController;
public HeroMasterViewController (IntPtr handle) : base (handle, Navigation.Page.HeroMaster)
{
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
_heroPageViewController = this.Storyboard.InstantiateViewController ("HeroPageViewController") as UIPageViewController;
_heroPageViewController.DataSource = this;
InitViewControllers ();
var startingViewController = new UIViewController[1] { _heroViewControllers [0] };
_heroPageViewController.SetViewControllers (startingViewController, UIPageViewControllerNavigationDirection.Forward, true, null);
_heroPageViewController.View.Frame = new CoreGraphics.CGRect (0, 0, this.View.Frame.Width, this.View.Frame.Size.Height);
this.AddChildViewController (_heroPageViewController);
this.View.AddSubview(_heroPageViewController.View);
_heroPageViewController.DidMoveToParentViewController(this);
}
public void InitViewControllers()
{
_heroViewControllers = new UIViewController[2];
_heroViewControllers [0] = this.Storyboard.InstantiateViewController ("HomeViewController");
_heroViewControllers [1] = this.Storyboard.InstantiateViewController ("CreateViewController");
}
public UIViewController GetPreviousViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
var controller = referenceViewController as HeroViewControllerBase;
if (controller.Index == 0)
return null;
return _heroViewControllers[controller.Index - 1];
}
public UIViewController GetNextViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
var controller = referenceViewController as HeroViewControllerBase;
if (controller.Index == (_heroViewControllers.Length - 1))
return null;
return _heroViewControllers[controller.Index + 1];
}
}
CreateViewController class:
partial class CreateViewController : HeroViewControllerBase
{
private Xamarin.Media.MediaPicker Picker;
private MediaPickerController MediaController;
public CreateViewController (IntPtr handle) : base (handle, Navigation.Page.HeroCreate, 1)
{
this.Picker = new Xamarin.Media.MediaPicker();
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
DisplayPicker ();
}
private void DisplayPicker()
{
if (this.MediaController != null)
return;
this.MediaController = this.Picker.GetTakePhotoUI(new StoreCameraMediaOptions
{
Name = DateTime.Now.ToFileTimeUtc() + ".jpg",
Directory = "MediaPickerSample"
});
this.MediaController.AllowsEditing = false;
this.MediaController.ShowsCameraControls = false;
this.MediaController.AllowsImageEditing = false;
this.MediaController.CameraDevice = UIImagePickerControllerCameraDevice.Front;
this.MediaController.View.Frame = new CGRect (0, 0, this.View.Frame.Width, this.View.Frame.Height);
var translate = CGAffineTransform.MakeTranslation (0, 71.0f);
this.View.AddSubview(this.MediaController.View);
}
}
I am developing an app for iOS using MvvmCross. On one of my Views I have some basic report data that is displayed in a tableview.
When the table row is touched a new view containing a detail report is displayed by making the call to ShowViewModel passing some parameters in a Dictionary. This works fine.
When the user swipes left or right the app needs to show the detail report for the next or previous item in the original list. I am doing this by updating some parameters and calling ShowViewModel again. The logic behind this is all working fine.
My problem; ShowViewModel animates the new view coming in from the right. This is perfect when the user has swiped left. However when swiping right it seems counter intuitive. How can I make ShowViewModel animate or transition in from the left side?
if you look to the MvvmCross source code here you see how the default behavior is showing the ViewControllers
You need to change that by doing something like the following:
How to change the Push and Pop animations in a navigation based app
for that, one idea is to have a custom view presenter and catch navigation to that particular view-model (override Show(IMvxTouchView view) )
or, maybe derive from UINavigationController, set it to MvvmCross to use it (look to the MvxSetup), and on some events change transition to that particular view
similar to this question
How to specify view transitions on iPhone
This is the solution I was able to come up with following the helpful pointers in the answer from Andrei N. In the end I opted for a TransitionFlipFromRight and TransitionFlipFromLeft when scrolling between detail reports. Hopefully it is useful to somebody else.
I already had a presenter class that was inherited from MvxModalSupportTouchViewPresenter
public class BedfordViewPresenter : MvxModalSupportTouchViewPresenter
Within this class I added a property of MvxPresentationHint.
private MvxPresentationHint _presentationHint;
In the override of method ChangePresentation the above property is used to store the passed in parameter
public override void ChangePresentation (MvxPresentationHint hint)
{
_presentationHint = hint;
base.ChangePresentation (hint);
}
Two new MvxPresentationHint class were declared (see later)
In the presenter class the Show method was overridden
public override void Show(IMvxTouchView view)
{
if (_presentationHint is FlipFromRightPresentationHint) {
var viewController = view as UIViewController;
MasterNavigationController.PushControllerWithTransition (viewController, UIViewAnimationOptions.TransitionFlipFromRight);
}
else
if (_presentationHint is FlipFromLeftPresentationHint) {
var viewController = view as UIViewController;
MasterNavigationController.PushControllerWithTransition (viewController, UIViewAnimationOptions.TransitionFlipFromLeft);
}
else {
base.Show (view);
}
_presentationHint = null;
}
A new class that provides extensions to a UINavigationController was created with the method PushControllerWithTransition
public static class UINavigationControllerExtensions
{
public static void PushControllerWithTransition(this UINavigationController
target, UIViewController controllerToPush,
UIViewAnimationOptions transition)
{
UIView.Transition(target.View, 0.75d, transition, delegate() {
target.PushViewController(controllerToPush, false);
}, null);
}
}
All that needs to be defined now are the two new MvxPresentationHint class derivations. These belong in your Core class library project rather than the iOS application project.
public class FlipFromLeftPresentationHint : MvxPresentationHint
{
public FlipFromLeftPresentationHint ()
{
}
}
and
public class FlipFromRightPresentationHint: MvxPresentationHint
{
public FlipFromRightPresentationHint ()
{
}
}
I hope this is a help to someone else trying to do something similar
Share my solution for android:
On view:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = base.OnCreateView(inflater, container, savedInstanceState);
var layout = view.FindViewById<LinearLayout>(Resource.Id.swippeable);
var swipeListener = new SwipeListener(this.Activity);
swipeListener.OnSwipeLeft += (sender, e) => this.ViewModel.LeftCommand?.Execute(); //Here use command into view model
swipeListener.OnSwipeRight += (sender, e) => this.ViewModel.RightCommand?.Execute();
layout.SetOnTouchListener(swipeListener);
return view;
}
Gesture listener:
public class SwipeListener : SimpleOnGestureListener, View.IOnTouchListener
{
private const int SWIPE_THRESHOLD = 100;
private const int SWIPE_VELOCITY_THRESHOLD = 100;
private readonly GestureDetector gestureDetector;
public SwipeListener(Context ctx)
{
this.gestureDetector = new GestureDetector(ctx, this);
}
public Boolean OnTouch(View v, MotionEvent e)
{
return this.gestureDetector.OnTouchEvent(e);
}
public event EventHandler OnSwipeRight;
public event EventHandler OnSwipeLeft;
public event EventHandler OnSwipeTop;
public event EventHandler OnSwipeBottom;
public override Boolean OnDown(MotionEvent e)
{
return true;
}
public override Boolean OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
Boolean result = false;
float diffY = e2.GetY() - e1.GetY();
float diffX = e2.GetX() - e1.GetX();
if (Math.Abs(diffX) > Math.Abs(diffY))
{
if (Math.Abs(diffX) > SWIPE_THRESHOLD && Math.Abs(velocityX) > SWIPE_VELOCITY_THRESHOLD)
{
if (diffX > 0)
{
SwipeRight();
}
else
{
SwipeLeft();
}
result = true;
}
}
else if (Math.Abs(diffY) > SWIPE_THRESHOLD && Math.Abs(velocityY) > SWIPE_VELOCITY_THRESHOLD)
{
if (diffY > 0)
{
SwipeBottom();
}
else
{
SwipeTop();
}
result = true;
}
return result;
}
public void SwipeRight()
{
this.OnSwipeRight?.Invoke(this, EventArgs.Empty);
}
public void SwipeLeft()
{
this.OnSwipeLeft?.Invoke(this, EventArgs.Empty);
}
public void SwipeTop()
{
this.OnSwipeTop?.Invoke(this, EventArgs.Empty);
}
public void SwipeBottom()
{
this.OnSwipeBottom?.Invoke(this, EventArgs.Empty);
}
}
I doing an application with various views types : MvxViewController, MvxTabBarViewController, ...
But when I want to do it, I meet difficulties : Following initial directives (http://bit.ly/1hLNMF3, http://bit.ly/1hNNY2g), I loose navigation back button among others.
So, I want to mix simple views and tabbed view without loosing Back button and without recode it (with NavigationItem.SetLeftBarButtonItem : http://bit.ly/1fsqGEC). Inspired by these solutions, I've do this :
A list of items (simple MvxTableController)
A detailed view of my items - MvxTabBarViewController, who have 3 tabs (1 view attached to each tab)
For the MvxTabBarViewController - main controller for item details :
public partial class SecondView : MvxTabBarViewController {
private int _count = 0;
public SecondView() {
ViewDidLoad();
}
public new SecondViewModel ViewModel {
get { return (SecondViewModel)base.ViewModel; }
set { base.ViewModel = value; }
}
public override void ViewDidLoad() {
base.ViewDidLoad();
if (ViewModel == null) return;
var viewControllers = new UIViewController[] {
CreateTabFor ("tab 1", "t1", ViewModel.Tab1);
CreateTabFor ("tab 2", "t2", ViewModel.Tab2);
CreateTabFor ("tab 3", "t3", ViewModel.Tab3);
}
ViewControllers = viewControllers;
CustomizableViewControllers = new UIViewController[0] { }
SelectedViewController = ViewControllers [0]
}
private UIViewController CreateTabFor (string tabTitle, string tabImage, IMvxViewModel viewModel) {
var controller = new UITabViewController ();
var screen = this.CreateViewControllerFor(viewModel) as UIViewController;
controller.TabBarItem = new UITabBarItem (tabTitle, UIImage.FromBundle("Images/" + tabImage + ".png"), _count);
_count++;
controller.Add (screen.View);
return controller;
}
}
Moreover, I don't need to change Setup class.
I am presenting a simple UIPageViewController and adding some really simple and stupid child view controllers to it. When the UIPageViewController gets dismissed I am disposing all child view controllers, the ones currently not displayed (listed in ChildViewControllers) and the one displayed (listed in ViewControllers). The not displayed ones get released, the displayed one gets not.
I have broken this down to a simple failing test, so I am sure it's not about the content of the child view controllers or some other issues around that. I have no idea what is retaining it.
Sample:
Master (presented)
public class MasterDialog : UIPageViewController
{
public event EventHandler OnDialogClosed;
private UIBarButtonItem _backButton;
public MasterDialog() : base(
UIPageViewControllerTransitionStyle.Scroll,
UIPageViewControllerNavigationOrientation.Horizontal,
UIPageViewControllerSpineLocation.None,
25)
{
_backButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel);
_backButton.Clicked += Close;
NavigationItem.SetLeftBarButtonItem(_backButton, false);
}
public override void ViewDidDisappear(bool animated)
{
base.ViewDidDisappear(animated);
OnDialogClosed(this, EventArgs.Empty);
}
private void Close(object sender, EventArgs arguments)
{
_backButton.Clicked -= Close;
NavigationController.DismissViewController(true, null);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Console.WriteLine("Master disposed");
}
}
Master Data Source
public class DataSource : UIPageViewControllerDataSource
{
public override UIViewController GetPreviousViewController(
UIPageViewController pageViewController, UIViewController referenceViewController)
{
var detail = (DetailDialog)referenceViewController;
if (detail.Page - 1 == 0)
return null;
return GetViewController(detail.Page - 1);
}
public override UIViewController GetNextViewController(
UIPageViewController pageViewController, UIViewController referenceViewController)
{
var detail = (DetailDialog)referenceViewController;
return GetViewController(detail.Page + 1);
}
public UIViewController GetViewController(int page)
{
return new DetailDialog(page);
}
}
Detail (Child)
public class DetailDialog : UITableViewController
{
public int Page { get; private set; }
public DetailDialog(int page) : base(UITableViewStyle.Plain)
{
Page = page;
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
Console.WriteLine("Detail init: " + Page + " / " + GetHashCode());
var label = new UILabel();
label.Text = "#" + Page;
label.ContentMode = UIViewContentMode.Center;
label.Frame = new System.Drawing.RectangleF(0, 100, 320, 50);
label.BackgroundColor = UIColor.Green;
Add(label);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Console.WriteLine("Detail disposed: " + Page + " / " + GetHashCode());
}
}
The opening dialog (starting point)
public class StartDialog : UIViewController
{
private DataSource _dataSource;
private MasterDialog _master;
public StartDialog()
{
Title = "WTF";
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var button = new UIButton(UIButtonType.Custom);
button.SetTitle("Open", UIControlState.Normal);
button.BackgroundColor = UIColor.Green;
button.Frame = new System.Drawing.RectangleF(20, 150, 280, 44);
Add(button);
button.TouchDown += OpenMasterDialog;
}
private void OpenMasterDialog(object sender, EventArgs arguments)
{
_dataSource = new DataSource();
_master = new MasterDialog();
_master.DataSource = _dataSource;
_master.OnDialogClosed += HandleOnDialogClosed;
_master.SetViewControllers(
new [] { _dataSource.GetViewController(1) },
UIPageViewControllerNavigationDirection.Forward,
false,
null
);
NavigationController.PresentViewController(
new UINavigationController(_master),
true,
null
);
}
private void HandleOnDialogClosed(object sender, EventArgs e)
{
_dataSource.Dispose();
_dataSource = null;
Console.WriteLine("Before: " + _master.ChildViewControllers.Length +
"/" + _master.ViewControllers.Length + ")");
var childs = _master
.ChildViewControllers.ToList()
.Union(_master.ViewControllers);
foreach (UIViewController child in childs)
{
child.RemoveFromParentViewController();
child.Dispose();
}
Console.WriteLine("After: " + _master.ChildViewControllers.Length +
"/" + _master.ViewControllers.Length + ")");
_master.OnDialogClosed -= HandleOnDialogClosed;
_master.Dispose();
_master = null;
}
}
I might be misunderstanding your code/intent but in this case it seems to me that everything is almost fine. Anyway here's my findings...
Detail disposed: 1 / 36217954
After: 0/1)
Line #2 shows /1 which I assume to be the issue. This is normal because you're re-surfacing the view controller, IOW the code:
_master.ViewControllers.Length
calls the viewControllers selector on the UIPageViewController. That returns: "The view controllers displayed by the page view controller." which is still DetailDialog at that point (even if master is not displayed anymore).
This is not Xamarin specific, an ObjC application would return the same (native) instance at that same point of time.
That's explained - but it still not freed later, why ?
Under the new Dispose semantics the managed object is kept, after Dispose, as long as the native side requires it (but without a native reference so it can be natively released and, subsequently, released on the managed side).
In this case the lifecycle of the native object is not yet over (i.e. iOS still has reference to it) so it remains alive on the managed side.
_master.Dispose();
_master = null;
This removes the managed references to _master but again (same as above) it won't be freed (and neither will be DetailDialog) as long as the native _master instance is used (with native references).
So who got a reference to _master ?
NavigationController.PresentViewController(
new UINavigationController(_master),
^ That creates a UINavigationController and as long as it's alive the there are references to the others.
When I dispose of the UINavigationController (I kept it in a field) then the Master* and Detail* instances disappear from HeapShot.
_nav.Dispose();
_nav = null;