Xamarin.iOS - Unable to load view controller from within another view controller - xamarin

I recently started developing using Xamarin, so I'm by no means an expert and have been stuck on this problem for a day or so now.
First of all, I am not using storyboards. I am creating my own custom views (xib) and loading them from code
I'm building a new Xamarin.iOS app and am attempting to load a view controller from within another view controller. Initially, I am loading the first controller from the AppDelegate like so:
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
window = new UIWindow(UIScreen.MainScreen.Bounds);
appStartUpController = new AppStartUpController();
window.RootViewController = appStartUpController;
window.MakeKeyAndVisible();
return true;
}
This loads my AppStartUpController fine which is basically just a loading screen with a background image and loading animation while I make an API call in the background. Once the API call has completed, I want to load another view controller.
After the API call has completed, I attempt to load the next Controller like so:
var controller = new CityPickerViewController();
this.NavigationController.PushViewController(controller, false);
And here is my CityPickerViewController:
public partial class CityPickerViewController : UIViewController
{
CityPicker_View v;
public CityPickerViewController(IntPtr handle) : base(handle)
{
}
public CityPickerViewController ()
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
v = CityPicker_View.Create();
this.View = v;
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(false);
UIImage i = UIImage.FromFile("citypickbackground.jpg");
i = i.Scale(this.View.Frame.Size);
this.View.BackgroundColor = UIColor.FromPatternImage(i);
}
}
I'm probably missing something obvious here, but the CityPickerViewController will not load. If I put a break point within the code, the viewDidLoad / ViewWillAppear overrides never get hit.
I'm a rookie programmer and would definitely appreciate any tips on this. Thanks in advance!

Welcome to SO!
Try base.NavigationController.PushViewController(controller, true); instead since you don't have a local navigation controller.
There could also be an issue in your CityPickerViewController, so try a different ViewController if that doesn't work.

Related

Failed to instantiate the default view controller for UIMainStoryboardFile

I am writing a small sample app with Xamarin and MvvmCross 6.4.2. I completed the Xamarin.Android version and am now starting the Xamarin.iOS version. I created a view controller for the first screen:
public class SignInViewController : MvxViewController<SignInViewModel>
{
public SignInViewController() : base(nameof(SignInViewController), null)
{
}
public override void ViewDidLoad()
{
// never gets called...
base.ViewDidLoad();
}
}
When I run the app, I just get a blank screen and ViewDidLoad never gets called. In the application output it says:
Failed to instantiate the default view controller for
UIMainStoryboardFile 'Main' - perhaps the designated entry point is
not set?
My Main.storyboard is blank and I tried to modify it in Xcode Interface Builder to set my SignInViewController as the entry point, but I couldn't figure out how.
Failed to instantiate the default view controller for
UIMainStoryboardFile 'Main' - perhaps the designated entry point is
not set?
This error happens due to a simple mistake in your storyboard. When your app starts, iOS needs to know precisely which view controller needs to be shown first – known as your default view controller.
To fix it, add a ViewController to your Main.Storyboard and set it as Inital View Controller.
Refer: how-to-fix-the-error-failed-to-instantiate-the-default-view-controller-for-uimainstoryboardfile
And there are two SignInViewController in your project.
Use this one:
public partial class SignInViewController : UIViewController
{
public SignInViewController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
// never gets called...
base.ViewDidLoad();
View.BackgroundColor = UIColor.Red;
}
}
Update:

How to use MvvmCross without storyboards in iOS?

When I was using MvvmCross 5, I coded all my views and avoided using storyboards for my iOS app by writing this in my AppDelegate:
[Register("AppDelegate")]
public class AppDelegate : MvxApplicationDelegate
{
private MvxIosViewPresenter viewPresenter;
public override UIWindow Window
{
get;
set;
}
/// <summary>
/// MvvmCross Mods:
/// - Creates a new presenter, which determines how Views are shown
/// - This example uses a standard presenter.
/// </summary>
/// <param name="application"></param>
/// <param name="launchOptions"></param>
/// <returns></returns>
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
// create a new window instance based on the screen size
Window = new UIWindow(UIScreen.MainScreen.Bounds);
// MvvmCross Mod Start---------------------------------------------
// This class will determine how Views are shown
this.viewPresenter = new MvxIosViewPresenter(this, Window);//new ViewPresenter(Window);
// Init the Setup object, which initializes App.cs
var setup = new Setup(this, this.viewPresenter);
setup.Initialize();
//this.viewPresenter.PresentModalViewController(new ListenViewController(), true);
// Use IoC to find and start the IMvxAppStart object
var startup = Mvx.Resolve<IMvxAppStart>();
startup.Start();
// MvvmCross Mod End--------------------------------------------------
// make the window visible
Window.MakeKeyAndVisible();
return true;
}
public class Setup : MvxIosSetup
{
public Setup(MvxApplicationDelegate appDelegate, IMvxIosViewPresenter presenter)
: base(appDelegate, presenter)
{
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
protected override IMvxTrace CreateDebugTrace()
{
return new DebugTrace();
}
}
But in MvvmCross 6.4.2, MvxIosSetup does not have the base constructor that takes 2 arguments. Instead I have:
[Register(nameof(AppDelegate))]
public partial class AppDelegate : MvxApplicationDelegate<Setup, App>
{
}
public class Setup : MvxIosSetup<App>
{
}
How can I configure it so that I can code my views without storyboards?
EDIT
I created a very small sample app with 1 view model/controller, using MvvmCross 7. The ViewDidLoad method never gets called in my MainViewController. Can someone please tell me why? I put my code here:
https://github.com/sinight95/TestApp/tree/main/TestApp
Neither the Setup.cs nor AppDelegate.cs files have anything to do whether you are presenting a storyboard or not. It is usually all up to which Presentation Attributes you are applying to a ViewController.
However, there are some stuff set in the Info.plist that changes how MvvmCross expects things to be set up.
Now in your example App you've put up on GitHub, you can do the following to make it not use the storyboard:
Remove Main.storyboard
In Info.plist set Main interface to (none)
In Info.plist set launch images to LaunchScreen.storyboard
Remove the scene delegate stuff in Info.plist
Remove the constructor in MainViewController
What is the main issue here is that you are essentially telling iOS that the ViewController has a storyboard by supplying this constuctor:
public MainViewController() : base(nameof(MainViewController), null)
{
}
Also are telling iOS to use storyboards through the Info.plist with all the scene delegation stuff.
I've created a PR showing what needs to be changed to make it run without the storboard and show the blue background color you've set on your ViewController.
https://github.com/sinight95/TestApp/pull/1

OnAppearing different on iOS and Android

I have found that on iOS, OnAppearing is called when the page literally appears on the screen, whereas on Android, it's called when it's created.
I'm using this event to lazily construct an expensive to construct view but obviously the Android behaviour defeats this.
Is there some way of knowing on Android when a screen literally appears on the screen?
You can use the event:
this.Appearing += YourPageAppearing;
Otherwise, you should use the methods of the Application class that contains the lifecycle methods:
protected override void OnStart()
{
Debug.WriteLine ("OnStart");
}
protected override void OnSleep()
{
Debug.WriteLine ("OnSleep");
}
protected override void OnResume()
{
Debug.WriteLine ("OnResume");
}
On Android, Xamarin.Forms.Page.OnAppearing is called immediately before the page's view is shown to user (not when the page is "created" (constructed)).
If you want an initial view to appear quickly, by omitting an expensive sub-view, use a binding to make that view's IsVisible initially be "false". This will keep it out of the visual tree, avoiding most of the cost of building it. Place the (invisible) view in a grid cell, whose dimensions are constant (either in DPs or "*" - anything other than "Auto".) So that layout will be "ready" for that view, when you make it visible.
APPROACH 1:
Now you just need a binding in view model that will change IsVisible to "true".
The simplest hack is to, in OnAppearing, fire an action that will change that variable after 250 ms.
APPROACH 2:
The clean alternative is to create a custom page renderer, and override "draw".
Have draw, after calling base.draw, check an action property on your page.
If not null, invoke that action, then clear it (so only happens once).
I do this by inheriting from a custom page base class:
XAML for each of my pages (change "ContentPage" to "exodus:ExBasePage"):
<exodus:ExBasePage
xmlns:exodus="clr-namespace:Exodus;assembly=Exodus"
x:Class="YourNamespace.YourPage">
...
</exodus:ExBasePage>
xaml.cs:
using Exodus;
// After creating page, change "ContentPage" to "ExBasePage".
public partial class YourPage : ExBasePage
{
...
my custom ContentPage. NOTE: Includes code not needed for this, related to iOS Safe Area and Android hardward back button:
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
namespace Exodus
{
public abstract partial class ExBasePage : ContentPage
{
public ExBasePage()
{
// Each sub-class calls InitializeComponent(); not needed here.
ExBasePage.SetupForLightStatusBar( this );
}
// Avoids overlapping iOS status bar at top, and sets a dark background color.
public static void SetupForLightStatusBar( ContentPage page )
{
page.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea( true );
// iOS NOTE: Each ContentPage must set its BackgroundColor to black or other dark color (when using LightContent for status bar).
//page.BackgroundColor = Color.Black;
page.BackgroundColor = Color.FromRgb( 0.3, 0.3, 0.3 );
}
// Per-platform ExBasePageRenderer uses these.
public System.Action NextDrawAction;
/// <summary>
/// Override to do something else (or to do nothing, i.e. suppress back button).
/// </summary>
public virtual void OnHardwareBackButton()
{
// Normal content page; do normal back button behavior.
global::Exodus.Services.NavigatePopAsync();
}
}
}
renderer in Android project:
using System;
using Android.Content;
using Android.Views;
using Android.Graphics;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Exodus;
using Exodus.Android;
[assembly: ExportRenderer( typeof( ExBasePage ), typeof( ExBasePageRenderer ) )]
namespace Exodus.Android
{
public class ExBasePageRenderer : PageRenderer
{
public ExBasePageRenderer( Context context ) : base( context )
{
}
protected override void OnElementChanged( ElementChangedEventArgs<Page> e )
{
base.OnElementChanged( e );
var page = Element as ExBasePage;
if (page != null)
page.firstDraw = true;
}
public override void Draw( Canvas canvas )
{
try
{
base.Draw( canvas );
var page = Element as ExBasePage;
if (page?.NextDrawAction != null)
{
page.NextDrawAction();
page.NextDrawAction = null;
}
}
catch (Exception ex)
{
// TBD: Got Disposed exception on Android Bitmap, after rotating phone (in simulator).
// TODO: Log exception.
Console.WriteLine( "ExBasePageRenderer.Draw exception: " + ex.ToString() );
}
}
}
}
To do some action after the first time the page is drawn:
public partial class YourPage : ExBasePage
{
protected override void OnAppearing()
{
// TODO: OnPlatform code - I don't have it handy.
// On iOS, we call immediately "DeferredOnAppearing();"
// On Android, we set this field, and it is done in custom renderer.
NextDrawAction = DeferredOnAppearing;
}
void DeferredOnAppearing()
{
// Whatever you want to happen after page is drawn first time:
// ((MyViewModel)BindingContext).ExpensiveViewVisible = true;
// Where MyViewModel contains:
// public bool ExpensiveViewVisible { get; set; }
// And your XAML contains:
// <ExpensiveView IsVisible={Binding ExpensiveViewVisible}" ... />
}
}
NOTE: I do this differently on iOS, because Xamarin Forms on iOS (incorrectly - not to spec) calls OnAppearing AFTER the page is drawn.
So I have OnPlatform logic. On iOS, OnAppearing immediately calls DeferredOnAppearing. On Android, the line shown is done.
Hopefully iOS will eventually be fixed to call OnAppearing BEFORE,
for consistency between the two platforms.
If so, I would then add a similar renderer for iOS.
(The current iOS implementation means there is no way to update a view before it appears a SECOND time, due to popping the nav stack.
instead, it appears with outdated content, THEN you get a chance
to correct it. This is not good.)

Windows Phone Back button and page instance creation

I need to recreate new page instance on every page load (also when user pressed Back button).
So I overrided OnBackKeyPress method:
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (NavigationService.CanGoBack) {
e.Cancel = true;
var j = NavigationService.RemoveBackEntry();
NavigationService.Navigate(j.Source);
NavigationService.RemoveBackEntry();
}
}
The problem is that I can't handle case when user press back button to close CustomMessageBox dialog. How can I check it? Or is there any way to force recreation of page instance when going back through history state?
Why do you need to recreate the page instance? If you are simply trying to re-read the data to be displayed, why not put the data loading logic into OnNavigatedTo()?
Assuming that is what you are actually trying to achieve, try something like this...
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
// don't do your data loading here. This will only be called on page creation.
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadData();
base.OnNavigatedTo(e);
}
MyViewModel model;
async void LoadData()
{
model = new MyViewModel();
await model.LoadDataAsync();
}
}
If you also have specific logic that you need to run on first construction of the page vs. on a back key navigation, check the NavigationMode property of the NavigationEventArgs object that gets passed to OnNavigatedTo.
if(e.NavigationMode == NavigationMode.New)
{
//do what you need to do specifically for a new page instance
}
if (e.NavigationMode == NavigationMode.Back)
{
// do anything specific for back navigation here.
}
Ha, in the near thread, i have opposite question :)
What about MessageBox - it depends, which one are you using. It can be custom message box, for example. Anyway, try to check MessageBox.IsOpened (or alternative for your MessageBox) in your OnBackKeyPress().
Another solution is to use OnNavigatedTo() of the page you want to be new each time.
Third solution: in case you works with Mvvm Light, add some unique id in ViewModel getter, like
public MyViewModel MyViewModel
{
get
{
return ServiceLocator.Current.GetInstance<MyViewModel>((++Uid).ToString());
}
}
This would force to recreate new ViewModel each time, so you'd have different instance of VM, so you would have another data on the View.

How to Cleanup a ViewModel in Mvvm Light?

I have a list of items that goes to another page, That page is hooked up to a view model. In the constructor of this view model I have code that grabs data from the server for that particular item.
What I found is that when I hit the back button and choose another item fromt hat list and it goes to the other page the constructor does not get hit.
I think it is because the VM is now created and thinks it does not need a new one. I am wondering how do I force a cleanup so that a fresh one is always grabbed when I select from my list?
I faced the same issue, that's how i solved it.
Have a BaseView class, override OnNavigatedTo
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (NavigatedToCommand != null && NavigatedToCommand.CanExecute(null))
NavigatedToCommand.Execute(null);
}
add DependencyProperty.
public static readonly DependencyProperty NavigatedToCommandProperty =
DependencyProperty.Register("NavigatedToCommand", typeof(ICommand), typeof(BaseView), null);
public ICommand NavigatedToCommand
{
get { return (ICommand)GetValue(NavigatedToCommandProperty); }
set { SetValue(NavigatedToCommandProperty, value); }
}
On the necessary pages, add to xaml (and, of course, inherit BaseView )
NavigatedToCommand="{Binding OnNavigatedToCommand}"
In the ViewModel, make command itself
public RelayCommand OnNavigatedToCommand
{ get { return new RelayCommand(OnNavigatedTo); } }
and implement method you want to call to update list
public async void OnNavigatedTo()
{
var result = await myDataService.UpdateMyList();
if (result.Status == OK)
MyList = result.List;
}
So, now, every time you navigate to page with list, inside of overriden OnNavigatedTo(), a NavigatedToCommand would be executed, which would execute OnNavigatedToCommand (which you set in xaml), which would call OnNavigatedTo, which would update your list.
A bit messy, but MVVM :)
EDIT: What about cleanings, they can be done in OnNavigatedFrom(), which works the same. Or OnNavigatingFrom(), which also can be useful in some cases.

Resources