I'm experiencing some very odd behavior when using UIPageViewController in my app. On the first swipe to the next view or anytime the direction of swiping changes (i.e. at the end of the list and going back), the resulting view is blank and I am required to swipe the same direction again for the appropriate view to show.
Example workflow with 3 views:
First view presented
Swipe to the right
Second view blinks in and out
Swipe to the right
Second view presented
Swipe to the right
Third view presented
Swipe to the left
Second view blinks in and out
Swipe to the left
Second view presented
I noticed via logging when the blinking happens (2 & 3 above), the sequence of events is as follows:
GetNextViewController is called
GetPreviousViewController is called
GetNextViewController is called
Then when I swipe again GetNextViewController is appropriately called and the view is shown.
My problem is, obviously, the second view should show without the blinking in and out. I have tried various things but nothing fruitful.
UPDATE
Here is a sample single-controller app that reproduces the problem:
using System;
using System.Drawing;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System.Collections.Generic;
namespace TestPageView
{
public partial class TestPageViewViewController : UIViewController
{
public TestPageViewViewController () : base ("TestPageViewViewController", null)
{
}
public override void DidReceiveMemoryWarning ()
{
// Releases the view if it doesn't have a superview.
base.DidReceiveMemoryWarning ();
// Release any cached data, images, etc that aren't in use.
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
var controllers = new List<UIViewController>
{
new TestViewController(View, 1),
new TestViewController(View, 2),
new TestViewController(View, 3),
new TestViewController(View, 4)
};
var _pageViewController = new UIPageViewController(UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation.Horizontal);
var dataSource = new ViewDataSource(controllers);
_pageViewController.DataSource = dataSource;
//ViewDataSource has an indexer that returns it's controllers
_pageViewController.SetViewControllers(new[] { controllers[0] }, UIPageViewControllerNavigationDirection.Forward, false, null);
View.AddSubview(_pageViewController.View);
}
public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
{
// Return true for supported orientations
return (toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown);
}
}
public class TestViewController : UIViewController
{
private readonly UIView _parent;
private readonly int _number;
public TestViewController(UIView parent, int number)
{
_parent = parent;
_number = number;
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
var textView = new UITextView(_parent.Bounds);
textView.TextAlignment = UITextAlignment.Center;
textView.AttributedText = new NSAttributedString(_number.ToString(), UIFont.BoldSystemFontOfSize(18));
View.Add(textView);
}
}
public class ViewDataSource : UIPageViewControllerDataSource
{
private LinkedListNode<UIViewController> current;
public ViewDataSource(IEnumerable<UIViewController> controllers)
{
var controllerLiist = new LinkedList<UIViewController>(controllers);
current = controllerLiist.First;
}
public override UIViewController GetPreviousViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
{
if(current.Previous == null){
Console.WriteLine("returning previous nothing");
return null;
}
Console.WriteLine("returning previous something");
current = current.Previous;
return current.Value;
}
public override UIViewController GetNextViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
{
if(current.Next == null){
Console.WriteLine("returning next nothing");
return null;
}
Console.WriteLine("returning next something");
current = current.Next;
return current.Value;
}
}
}
iOS will call GetPreviousViewController and GetNextViewController twice: first for current controller and second for backflip of current controller.
You should do that
current = current.Previous;
only at first call.
Check referenceViewController to do that:
public override UIViewController GetPreviousViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
var __c = Pages.Find(referenceViewController);
if (__c.Previous != null)
return __c.Previous.Value;
else
return null;
}
public override UIViewController GetNextViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
var __c = Pages.Find(referenceViewController);
if (__c.Next != null)
return __c.Next.Value;
else
return null;
}
Related
I have an application written with Xamarin for IOS and Android where I have a list of contacts and when I click on one of them I want to open the according contact in the addressbook of the phone. I have an interface for that implement the method on IOS that way:
private void ShowContactDialog(string addressbookId, bool isEdit = false)
{
var contact = new AddressBookService().GetCnContactById(addressbookId);
var view = CNContactViewController.FromContact(contact);
view.Editing = isEdit;
DisplayView(view);
}
private static void DisplayView(CNContactViewController view)
{
var window = UIApplication.SharedApplication.KeyWindow;
var vc = window.RootViewController;
while (vc.PresentedViewController != null)
{
vc = vc.PresentedViewController;
}
if (vc is UINavigationController navController)
{
vc = navController.ViewControllers.Last();
}
vc.PresentViewController(new UINavigationController(view), true, null);
}
That works so far as it opens the contact:
The issue now is, that there is just an edit button, but now Done, cancel or back button. Therefore I have to kill the whole application and start it again to come back.
Is there a way to add a extra button similar to the edit button to dismiss the dialog again?
EDIT: I adjusted the code as suggested in the comments by Kevin Li.
private void ShowContactDialog(string addressbookId, bool isEdit = false)
{
var contact = new AddressBookService().GetCnContactById(addressbookId);
var view = CNContactViewController.FromContact(contact);
view.Editing = isEdit;
GetCurrentViewController().NavigationController.PushViewController(view, true);
}
private static UIViewController GetCurrentViewController()
{
var window = UIApplication.SharedApplication.KeyWindow;
var vc = window.RootViewController;
while (vc.PresentedViewController != null)
{
vc = vc.PresentedViewController;
}
if (vc is UINavigationController navController)
{
vc = navController.ViewControllers.Last();
}
return vc;
}
Now it is shown within the Tabs:
Unfortunately is the back button not displayed as well.
I tried to make a new UINavigationController, but that didn't change anything.
EDIT2:
I adjusted the way the tabs are created another time. My MainViewController looks like that:
[MvxRootPresentation(WrapInNavigationController = true)]
public class MainViewController : MvxTabBarViewController<MainViewModel>
{
private bool constructed;
private bool firstTimePresented = true;
public MainViewController()
{
constructed = true;
// need this additional call to ViewDidLoad because UIkit creates the view before the C# hierarchy has been constructed
ViewDidLoad();
}
public override async void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (firstTimePresented)
{
// Initialize the tabs
firstTimePresented = false;
await ViewModel.GoToContactListCommand.ExecuteAsync();
await ViewModel.GoToBirthdayListCommand.ExecuteAsync();
await ViewModel.GoToProfileListCommand.ExecuteAsync();
await ViewModel.GoToSettingsCommand.ExecuteAsync();
}
}
public override void ViewDidLoad()
{
if (!constructed)
return;
base.ViewDidLoad();
Title = "Connect Update";
EdgesForExtendedLayout = UIRectEdge.None;
View.BackgroundColor = UIColor.White;
}
}
On each View who shall be a tab has to be this Attribute:
[MvxTabPresentation(TabIconName = "IconCake", TabName = "Birthdays")]
public partial class BirthdayView : MvxViewController<BirthdayViewModel>
The code for opening the contact is the same as above. Now it makes a new View without the tabs but with the back button, exactly as wished :)
The issue now is, that there is just an edit button, but now Done, cancel or back button.
It is because the value of isEdit at view.Editing = isEdit; is false. So, when you present the CNContactViewController, there's only an Edit button. When you click the Edit button, it will change to the editing mode and the Cancel and Done buttons will appear.
Or you can set the value of isEdit to true, then the Cancel and Done buttons will show when the CNContactViewController presents.
Therefore I have to kill the whole application and start it again to come back.
Is there a way to add a extra button similar to the edit button to dismiss the dialog again?
You don't have to kill and restart the app to dismiss the dialog. You can just implement the DidComplete in the delegate: CNContactViewControllerDelegate to dismiss the dialog when user finishes editing the contact and click the Done button or cancel it with clicking the Cancel button.
Here's the related code snippet:
var view = CNContactViewController.FromContact(contact);
view.Delegate = new MyCNConatactViewControllerDelegate();
public class MyCNConatactViewControllerDelegate : CNContactViewControllerDelegate
{
public override void DidComplete(CNContactViewController viewController, Contacts.CNContact contact)
{
viewController.NavigationController.DismissViewController(true, null);
}
}
Update:
To show the Back button in the CNContactViewController:
Make sure the UI hierarchy like this: Navigation->Viewcontroller(Page)->CNContactViewController. Then change the method from Present to Push to show the CNContactViewController, like this:
this.NavigationController.PushViewController(view, true);
I have two iOS views marked by MvxRootPresentation attribute: LoginView without wrapping into navigation controller and MainView with wrapping into navigation controller.
When I call ShowViewModel<MainViewModel>() there is no animation between these two views. All subsequent views are animated as usual (within NavigationController).
How can I set animation for this transition?
Ok, I've did it myself :) I had to add my custom presentation attribute and custom presenter:
public class AnimatedRootPresentationAttribute : MvxRootPresentationAttribute
{
}
public class MyPresenter : MvxIosViewPresenter
{
public MyPresenter(IUIApplicationDelegate appDelegate, UIWindow window)
: base(appDelegate, window)
{
}
protected override void RegisterAttributeTypes()
{
base.RegisterAttributeTypes();
_attributeTypesToShowMethodDictionary.Add(typeof(AnimatedRootPresentationAttribute),
(viewController, attribute, request) => ShowAnimatedRootViewController(
viewController, (AnimatedRootPresentationAttribute)attribute, request));
}
private void ShowAnimatedRootViewController(
UIViewController viewController,
AnimatedRootPresentationAttribute attribute,
MvxViewModelRequest request)
{
ShowRootViewController(viewController, attribute, request);
AddAnimation();
}
private void AddAnimation()
{
var transition = new CATransition
{
Duration = 0.2,
Type = CAAnimation.TransitionMoveIn,
Subtype = CAAnimation.TransitionFromTop
};
_window.RootViewController.View.Layer.AddAnimation(transition, null);
}
}
I have a Xamarin.iOS app where I have the following structure:
NavController (root: Login)
--> TabBarController (Home) - (Search) - (Profile)
------> NavController (root: Home)
------------->TableController
----------------->DetailController
------>NavController (root: Search)
...etc
I am currently having difficulties working with the navigation items, specifically the back button item.
I never want to go back to the login page via the back button, so in my HomeController, I hide the back button by saying
TabBarController.NavigationItem.HidesBackButton = true;
When I go to the next screen (TableController) I want to see the back button that goes back to the HomeController, however my current approach has a back button to the Login Controller
this.TabBarController.NavigationItem.HidesBackButton = false;
Thanks for any help
For the effect you want, I suggest you create an object(I use "RootController" as an example) to manage the app's Window.RootController.
I don't know if you have some experience for change window's RootController, so just follow my step:
At first create a new project, remove the storyboard and Viewcontroller.cs.(Don't forget remove the bundle for storyboard in the info.plist)
Then rewrite your AppDelegate.cs, like this:
[Register ("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
UIWindow window;
public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
{
// create a new window instance based on the screen size
window = new UIWindow (UIScreen.MainScreen.Bounds);
window.RootViewController = RootController.Instance.LoginController;
window.MakeKeyAndVisible ();
return true;
}
public override void OnResignActivation (UIApplication application)
{
// Invoked when the application is about to move from active to inactive state.
// This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message)
// or when the user quits the application and it begins the transition to the background state.
// Games should use this method to pause the game.
}
public override void DidEnterBackground (UIApplication application)
{
// Use this method to release shared resources, save user data, invalidate timers and store the application state.
// If your application supports background exection this method is called instead of WillTerminate when the user quits.
}
public override void WillEnterForeground (UIApplication application)
{
// Called as part of the transiton from background to active state.
// Here you can undo many of the changes made on entering the background.
}
public override void OnActivated (UIApplication application)
{
// Restart any tasks that were paused (or not yet started) while the application was inactive.
// If the application was previously in the background, optionally refresh the user interface.
}
public override void WillTerminate (UIApplication application)
{
// Called when the application is about to terminate. Save data, if needed. See also DidEnterBackground.
}
}
And this is RootController.cs which include a UINavigationController and a UITabBarController(Home - Search - Profile):
public class RootController
{
private static RootController instance;
public static RootController Instance {
get {
if (instance == null)
instance = new RootController ();
return instance;
}
}
private static UINavigationController loginController;
public UINavigationController LoginController {
get {
if (loginController == null)
InitLoginController ();
return loginController;
}
}
private void InitLoginController()
{
UIViewController loginViewController = new UIViewController (){ Title = "LoginController" };
loginViewController.View.Frame = UIScreen.MainScreen.Bounds;
loginViewController.View.BackgroundColor = UIColor.Red;
loginViewController.NavigationItem.SetRightBarButtonItem (new UIBarButtonItem ("MainTab",UIBarButtonItemStyle.Done, delegate {
UIApplication.SharedApplication.KeyWindow.RootViewController = RootController.Instance.MainTabController;
}),true);
loginController = new UINavigationController (loginViewController);
}
private static UITabBarController mainTabController;
public UITabBarController MainTabController {
get {
if (mainTabController == null)
InitMainTabController ();
return mainTabController;
}
set {
mainTabController = value;
}
}
private void InitMainTabController ()
{
mainTabController = new UITabBarController ();
mainTabController.ViewControllers = new UIViewController [] {
new UINavigationController(new HomeViewController() {
TabBarItem = new UITabBarItem (UITabBarSystemItem.Favorites,0)
}),
new UINavigationController (new UIViewController ()
{
Title = "SearchController",
TabBarItem = new UITabBarItem (UITabBarSystemItem.Search,1)
}),
new UINavigationController (new UIViewController ()
{
Title = "ProfileController",
TabBarItem = new UITabBarItem (UITabBarSystemItem.More,2)
})
};
mainTabController.SelectedIndex = 0;
}
}
And this is HomeController.cs which can push an UITableViewController as you wish and also has a button to return to LoginController if you needed:
public class HomeViewController : UIViewController
{
public HomeViewController ()
{
Title = "HomeController";
this.View.Frame = UIScreen.MainScreen.Bounds;
this.View.BackgroundColor = UIColor.Green;
//Set a button to return to loginController
this.NavigationItem.SetLeftBarButtonItem (new UIBarButtonItem ("LoginC",UIBarButtonItemStyle.Done, delegate {
UIApplication.SharedApplication.KeyWindow.RootViewController = RootController.Instance.LoginController;
}),true);
//Set a button to go to tableController
UITableViewController tableViewController = new UITableViewController (){ Title = "TableViewController" };
tableViewController.View.Frame = UIScreen.MainScreen.Bounds;
tableViewController.View.BackgroundColor = UIColor.White;
tableViewController.HidesBottomBarWhenPushed = true;//To make tabbar disappear
this.NavigationItem.SetRightBarButtonItem (new UIBarButtonItem ("TableView",UIBarButtonItemStyle.Done, delegate {
this.NavigationController.PushViewController(tableViewController,true);
}),true);
}
}
You can add your own ViewController in RootController.cs instead of my sample controllers.
If you still have some problems, just leave it here, I'll check it latter.
Hope it can help you.
I'm creating a CocosSharp game with several additional native screens which I'm going to implement using native iOS UIViewController. The flow is the following:
On app startup I'm creating UINavigationViewController and initial view with my custom main menu:
public override void FinishedLaunching(UIApplication app)
{
MainWindow = new UIWindow(UIScreen.MainScreen.Bounds);
var rootViewStoryboard = UIStoryboard.FromName("GameMenu", null);
var rootView = rootViewStoryboard.InstantiateViewController("GameMenu");
var navigationController = new UINavigationController(rootView);
MainWindow.RootViewController = navigationController;
MainWindow.MakeKeyAndVisible();
}
public partial class GameMenu : UIViewController
{
public GameMenu(IntPtr handle)
: base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
btnStartGame.TouchUpInside += (sender, e) => NavigationController.PresentViewController(new GameScene(), false);
}
}
One of the actions leads to another UIViewController (modal) where CocosSharp game is created:
public partial class GameScene : UIViewController
{
public GameScene()
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
_application = new CCApplication();
_application.ApplicationDelegate = new GameAppDelegate();
_application.StartGame();
_application.MainWindow.DisplayStats = true;
OnStopGame();
}
}
Finally on certain event (in my case in 5 sec) I'm trying to finish a game and return back to the main menu:
public async Task OnStopGame()
{
await Task.Delay(5000);
_application.ExitGame();
NavigationController.DismissViewController(false, null);
}
As a result I get empty black screen, background music continue to run, no main menu is displayed.
How should I properly navigate from CocosSharp game to a native controller and back? Can they live in parallel?
For MVVMCross ios, how can I use different TransitionalStyle such as FlipHorizontal style instead of the default sliding effect with "ShowViewModel"?
[Register("SearchResults")]
public class SearchResultsView : MvxTableViewController
{
public override void ViewDidLoad()
{
Title = "List";
base.ViewDidLoad();
var mapButton = new UIButton(new RectangleF(0, 0, 65, 30));
mapButton.SetBackgroundImage(UIImage.FromBundle("images/map_btn.png"), UIControlState.Normal);
mapButton.TouchUpInside += MapButtonClicked();
var rightButton = new UIBarButtonItem(mapButton);
NavigationItem.RightBarButtonItem = rightButton;
var bindings = this.CreateBindingSet<SearchResultsView, SearchResultsViewModel>();
//bindings.Bind(mapButton).To(x => x.ShowMapCommand); //how to do with binding command?
bindings.Apply();
}
private EventHandler MapButtonClicked()
{
return (sender, args) =>
{
var mapView = new SearchResultMapView {ModalTransitionStyle = UIModalTransitionStyle.FlipHorizontal};
var navigationController = new UINavigationController(mapView);
PresentViewController(navigationController, true, null);
};
}
}
ViewModels/Views are presented using a Presenter.
For Modal displays of navigation controllers, some people use the MvxModalNavSupportTouchViewPresenter.cs
You can use this presenter by overriding CreatePresenter in Setup:
protected override IMvxTouchViewPresenter CreatePresenter()
{
return new MvxModalNavSupportTouchViewPresenter(ApplicationDelegate, Window);
}
With this done then you should be able to achieve your transition effect by adding IMvxModalTouchView inheritance and by setting ModalTransitionStyle = UIModalTransitionStyle.FlipHorizontal; in the constructor for your SearchResultMapView.
public class SearchResultMapView
: MvxViewController, IMvxModalTouchView
{
public SearchResultMapView()
{
ModalTransitionStyle = UIModalTransitionStyle.FlipHorizontal;
}
// more code here....
}
Alternatively, if you wanted to completely customise the presentation of the view/viewcontroller, then implementing a custom presenter is fairly straight-forward to do - for more information, see some of the articles and tutorials on custom presenters in http://slodge.blogspot.co.uk/2013/06/presenter-roundup.html
#wing
All you need to do is add a close button to the new view of the pushed view controller and override it's touchupinside as follows:
private void CloseBtnTouchUpInside(object sender, EventArgs eventArgs)
{
DismissModalViewController(false);
}