I have form in xamarin.forms and I want to show a popup message when users click on the nav bar button if there are pending data to save. I found this example but it doesn't not working on Xamarin.Forms 5.0
Any idea of how to do it?
I did a quick test on this you can refer to it.
First, I create a contentpage and set CustomBackButtonAction, EnableBackButtonOverride to add navigate method:
public partial class TestPage5 : ContentPage
{public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty = BindableProperty.Create(
nameof(EnableBackButtonOverride),
typeof(bool),
typeof(TestPage5),
false
);
public bool EnableBackButtonOverride {
get { return (bool)GetValue(EnableBackButtonOverrideProperty); }
set { SetValue(EnableBackButtonOverrideProperty, value); }
}
public TestPage5()
{
InitializeComponent();
EnableBackButtonOverride = true;
CustomBackButtonAction = async () => { var result = await DisplayAlert("Alert", "Are you Sure?", "Yes", "No");
if (result)
{ await Navigation.PopAsync(true); } };
}
}
Then create renderer on ios while override OnOptionsItemSelected on android:
ios(create a new backbutton and override):
[assembly:ExportRenderer(typeof(TestPage5),typeof(MyRenderer))]
namespace My_Forms_Test3.iOS
{
public class MyRenderer:Xamarin.Forms.Platform.iOS.PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((TestPage5)Element).EnableBackButtonOverride)
{
SetButton();
}
}
private void SetButton()
{
var backbuttonimg = UIImage.FromBundle("backarrow.png");
backbuttonimg = backbuttonimg.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
var backbutton = new UIButton(UIButtonType.Custom)
{ HorizontalAlignment=UIControlContentHorizontalAlignment.Left,
TitleEdgeInsets=new UIEdgeInsets(11.5f,15f,10f,0f),
ImageEdgeInsets=new UIEdgeInsets(1f,8f,0f,0f)};
backbutton.SetTitle("Back", UIControlState.Normal);
backbutton.SetTitleColor(UIColor.White, UIControlState.Normal);
backbutton.SetTitleColor(UIColor.LightGray, UIControlState.Highlighted);
backbutton.Font = UIFont.FromName("HelveticaNeue", (nfloat)17);
backbutton.SetImage(backbuttonimg, UIControlState.Normal);
backbutton.SizeToFit();
backbutton.TouchDown += (sender, e) =>
{
if (((TestPage5)Element)?.CustomBackButtonAction != null)
{
((TestPage5)Element)?.CustomBackButtonAction.Invoke();
}
};
backbutton.Frame = new CoreGraphics.CGRect(0, 0, UIScreen.MainScreen.Bounds.Width / 4,
NavigationController.NavigationBar.Frame.Height);
var buttoncontainer = new UIView(new CoreGraphics.CGRect(0, 0, backbutton.Frame.Width, backbutton.Frame.Height));
buttoncontainer.AddSubview(backbutton);
var fixspace = new UIBarButtonItem(UIBarButtonSystemItem.FixedSpace)
{ Width = -16f };
var backbuttonitem = new UIBarButtonItem("", UIBarButtonItemStyle.Plain, null) { CustomView = backbutton };
NavigationController.TopViewController.NavigationItem.LeftBarButtonItems = new[] { fixspace, backbuttonitem };
}
}
}
android:
add following on main activity:
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
//important to trigger OnOptionItemSelected
Android.Support.V7.Widget.Toolbar toolbar
= this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
also in mainactivity.cs:
public override bool OnOptionsItemSelected(IMenuItem item)
{
// check if the current item id
// is equals to the back button id
if (item.ItemId == 16908332) // xam forms nav bar back button id
{
// retrieve the current xamarin
// forms page instance
var currentpage = (TestPage5)Xamarin.Forms.Application.Current.
MainPage.Navigation.NavigationStack.LastOrDefault();
// check if the page has subscribed to the custom back button event
if (currentpage?.CustomBackButtonAction != null)
{
// invoke the Custom back button action
currentpage?.CustomBackButtonAction.Invoke();
// and disable the default back button action
return false;
}
// if its not subscribed then go ahead
// with the default back button action
return base.OnOptionsItemSelected(item);
}
else
{
// since its not the back button
//click, pass the event to the base
return base.OnOptionsItemSelected(item);
}
}
//android Hardware back button event
public override void OnBackPressed()
{
// this is really not necessary, but in Android user has both Nav bar back button
// and physical back button, so its safe to cover the both events
var currentpage = (BaseContentPage)Xamarin.Forms.Application.Current.
MainPage.Navigation.NavigationStack.LastOrDefault();
if (currentpage?.CustomBackButtonAction != null)
{
currentpage?.CustomBackButtonAction.Invoke();
}
else
{
base.OnBackPressed();
}
}
Here is the full blog I have written which handles the same,
Android:
I have used NavigationPage Renderer to achieve this functionality in android
Android Implementtion
iOS:
I have used Page Renderer to achieve this functionality in iOS
public class CustomPageRenderer:PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (Element != null && Element is BasePage basePage && basePage.BindingContext != null &&
basePage.BindingContext is BaseViewModel baseViewModel)
{
SetCustomBackButton(baseViewModel);
}
}
private void SetCustomBackButton(BaseViewModel baseViewModel)
{
UIButton btn = new UIButton();
btn.Frame = new CGRect(0, 0, 50, 40);
btn.BackgroundColor = UIColor.Clear;
btn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
baseViewModel.BackPressedAction?.Invoke(false);
};
//var views = NavigationController?.NavigationBar.Subviews;
NavigationController?.NavigationBar.AddSubview(btn);
}
}
Note:
Do create BackPressedAction Action in your base view model to capture the back press event
Related
I have a webview that starts dial tel number from url with OverrideUrlLoading and now after deploying the app events are not firing. How can I send an event after LoadUrl from custom WebViewClient?
public class OmaWeb : ContentPage{
...
frame = new Frame
{
Padding = new Thickness(5),
BackgroundColor = Color.White,
Opacity = 1,
CornerRadius = 10,
Content = loading
};
layout = new StackLayout
{
Margin = new Thickness(150, 300, 150, 0),
Children =
{
frame
}
};
//not firing
webView.Navigated += (object sender, WebNavigatedEventArgs args) =>{
layout.IsVisible = false; // should hide the layout of spinner
};
}
WebViewRender.cs
public class CustomWebViewClient : WebViewClient{
Activity mActivity = null;
public CustomWebViewClient(Activity activity){
mActivity = activity;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)
{
if (request.Url.ToString().StartsWith("tel:"))
{
Intent intent = new Intent(Intent.ActionDial, request.Url);
mActivity.StartActivity(intent);
}
else
{
view.LoadUrl(request.Url.ToString());
}
return true;
}
}
I solved the problem. What I did is I added a WebView renderer as parameter of my custom WebViewClient
WebviewRenderer _renderer = null;
public CustomWebViewClient(WebviewRenderer renderer, Activity activity)
{
mActivity = activity;
_renderer = renderer ?? throw new ArgumentNullException("renderer");
}
then I override OnPageFinished method to send navigated event.
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
var source = new UrlWebViewSource { Url = url };
var args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, WebNavigationResult.Success);
_renderer.ElementController.SendNavigated(args);
}
This article helped me
enter link here
I have an application using Xamarin Forms TabbedPage which has a feature that would allow the user to pause and play a page. Please see the code below.
Shared Code
public partial class MainPage : TabbedPage
{
public MainPage()
{
InitializeComponent();
var homePage = new NavigationPage(new HomePage())
{
Title = "Home",
Icon = "ionicons_2_0_1_home_outline_25.png"
};
var phrasesPage = new NavigationPage(new PhrasesPage())
{
Title = "Play",
Icon = "ionicons_2_0_1_play_outline_25.png"
};
Children.Add(homePage);
Children.Add(phrasesPage);
}
}
In iOS renderer:
public class TabbedPageRenderer : TabbedRenderer
{
private MainPage _page;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
var tabbarController = (UITabBarController)this.ViewController;
if (null != tabbarController)
{
tabbarController.ViewControllerSelected += OnTabBarReselected;
}
}
void OnTabBarReselected(object sender, UITabBarSelectionEventArgs e)
{
var tabs = Element as TabbedPage;
var playTab = tabs.Children[4];
if (TabBar.SelectedItem.Title == "Play") {
if (tabs != null)
{
playTab.Title = "Pause";
playTab.Icon = "ionicons_2_0_1_pause_outline_22.png";
}
App.pauseCard = false;
}
else {
if (tabs != null) {
playTab.Title = "Play";
playTab.Icon = "ionicons_2_0_1_play_outline_25.png";
}
App.pauseCard = true;
}
}
Android Renderer
public class MyTabbedPageRenderer: TabbedPageRenderer, TabLayout.IOnTabSelectedListener
{
if (e.PropertyName == "Renderer")
{
viewPager = (ViewPager)ViewGroup.GetChildAt(0);
tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
setup = true;
ColorStateList colors = null;
if ((int)Build.VERSION.SdkInt >= 23)
{
colors = Resources.GetColorStateList(Resource.Color.icon_tab, Forms.Context.Theme);
}
else
{
colors = Resources.GetColorStateList(Resource.Color.icon_tab);
}
for (int i = 0; i < tabLayout.TabCount; i++)
{
var tab = tabLayout.GetTabAt(i);
var icon = tab.Icon;
if (icon != null)
{
icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
Android.Support.V4.Graphics.Drawable.DrawableCompat.SetTintList(icon, colors);
}
}
}
void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
{
var tabs = Element as TabbedPage;
var playTab = tabs.Children[4];
var selectedPosition = tab.Position;
if(selectedPosition == 4)
{
if (playTab.Title == "Play")
{
if (tabs != null)
{
playTab.Title = "Pause";
playTab.Icon = "ionicons_2_0_1_pause_outline_22.png";
}
App.pauseCard = false;
}
else
{
if (tabs != null)
{
playTab.Title = "Play";
playTab.Icon = "ionicons_2_0_1_play_outline_25.png";
}
App.pauseCard = true;
}
}
}
}
This is perfectly working in iOS. But somehow in Android only the Title would change but not the Icon. Anyone knows what Im missing or how it should be done? Also, is this possible to be done in the shared code instead of repeating almost exactly the same lines on code in each platform?
You can do it by using the tab that is being passe to you in the OnTabReselected parameters in the TabRenderer.
You can move your whole logic with this object.
This is my whole renderer file (Android):
[assembly: ExportRenderer(typeof(SWTabSelection.MainPage), typeof(SWTabSelection.Droid.MyTabbedPageRenderer))]
namespace SWTabSelection.Droid
{
public class MyTabbedPageRenderer : TabbedPageRenderer, TabLayout.IOnTabSelectedListener
{
private ViewPager viewPager;
private TabLayout tabLayout;
private bool setup;
public MyTabbedPageRenderer() { }
public MyTabbedPageRenderer(Context context) : base(context)
{
//Use this constructor for newest versions of XF saving the context parameter
// in a field so it can be used later replacing the Xamarin.Forms.Forms.Context which is deprecated.
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "Renderer")
{
viewPager = (ViewPager)ViewGroup.GetChildAt(0);
tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
setup = true;
ColorStateList colors = GetTabColor();
for (int i = 0; i < tabLayout.TabCount; i++)
{
var tab = tabLayout.GetTabAt(i);
SetTintColor(tab, colors);
}
}
}
void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
{
// To have the logic only on he tab on position 1
if(tab == null || tab.Position != 1)
{
return;
}
if(tab.Text == "Play")
{
tab.SetText("Pause");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_pause_outline_25);
App.pauseCard = false;
}
else
{
tab.SetText("Play");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_play_outline_25);
App.pauseCard = true;
}
SetTintColor(tab, GetTabColor());
}
void SetTintColor(TabLayout.Tab tab, ColorStateList colors)
{
var icon = tab?.Icon;
if(icon != null)
{
icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
Android.Support.V4.Graphics.Drawable.DrawableCompat.SetTintList(icon, colors);
}
}
ColorStateList GetTabColor()
{
return ((int)Build.VERSION.SdkInt >= 23)
? Resources.GetColorStateList(Resource.Color.icon_tab, Forms.Context.Theme)
: Resources.GetColorStateList(Resource.Color.icon_tab);
}
}
}
The only thing that I had with the code above is that the icon was not taking the Tint color so created a function with the same logic you had to set the Tint and I am using it on the Tab Reselection. If you have only one tab in your app you can set a global tint in the Android Theme/Style xml.
Hope this helps.
Custom Renderer is no needed , you can change the Title and Icon directly in Shared code.
Just implement CurrentPageChanged event in TabbedPage
Complete code
public partial class TabbedPage1 : TabbedPage
{
NavigationPage homePage;
NavigationPage phrasesPage;
public TabbedPage1 ()
{
InitializeComponent();
var homePage = new NavigationPage(new Page1())
{
Title = "Home",
Icon = "1.png"
};
var phrasesPage = new NavigationPage (new Page2())
{
Title = "Play",
Icon = "1.png"
};
Children.Add(homePage);
Children.Add(phrasesPage);
this.CurrentPageChanged += (object sender, EventArgs e) => {
var i = this.Children.IndexOf(this.CurrentPage);
if (i == 0)
{
homePage.Title = "HomeChanged";
homePage.Icon = "2.png";
}
else {
phrasesPage.Title = "PlayChanged";
phrasesPage.Icon = "2.png";
}
};
}
}
Result
PS: Make the image files access from a different platform.
iOS - Resources
Android - Resources->drawable
There isn't a way to detect when a tab is reselected in Xamarin.Forms, so we'll have to use custom rederers to detect the logic.
For Android, we'll have to handle two cases: Current Tab Page Changed, and Current Tab Page Reselected.
We'll subscribe to to CurrentPageChanged and in its EventHandler, we'll check to see if the tab selected is PhrasesPage. If it is, we'll update the Icon/Text.
In OnTabReselected, we can check which page is currently selected, and if it's the PhrasesPage, we can update PhrasesPage.Icon and PhrasesPage.Text.
Sample App
https://github.com/brminnick/ChangeTabbedPageIconSample/tree/master
Android Custom Renderer
[assembly: ExportRenderer(typeof(MainPage), typeof(TabbedPageRenderer))]
namespace YourNameSpace
{
public class TabbedPageRenderer : TabbedRenderer, TabLayout.IOnTabSelectedListener
{
//Overloaded Constructor required for Xamarin.Forms v2.5+
public TabbedPageRenderer(Android.Content.Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e)
{
base.OnElementChanged(e);
Element.CurrentPageChanged += HandleCurrentPageChanged;
}
void HandleCurrentPageChanged(object sender, EventArgs e)
{
var currentNavigationPage = Element.CurrentPage as NavigationPage;
if (!(currentNavigationPage.RootPage is PhrasesPage))
return;
var tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
for (int i = 0; i < tabLayout.TabCount; i++)
{
var currentTab = tabLayout.GetTabAt(i);
var currentTabText = currentTab.Text;
if (currentTabText.Equals("Play") || currentTabText.Equals("Pause"))
{
Device.BeginInvokeOnMainThread(() => UpdateTab(currentTabText, currentTab, currentNavigationPage));
break;
}
}
}
void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
{
System.Diagnostics.Debug.WriteLine("Tab Reselected");
var mainPage = Application.Current.MainPage as MainPage;
var currentNavigationPage = mainPage.CurrentPage as NavigationPage;
if(currentNavigationPage.RootPage is PhrasesPage)
Device.BeginInvokeOnMainThread(() => UpdateTab(currentNavigationPage.Title, tab, currentNavigationPage));
}
void UpdateTab(string currentTabText, TabLayout.Tab tab, NavigationPage currentNavigationPage)
{
if (currentTabText.Equals("Puzzle"))
{
tab.SetIcon(IdFromTitle("Settings", ResourceManager.DrawableClass));
currentNavigationPage.Title = "Settings";
}
else
{
tab.SetIcon(IdFromTitle("Puzzle", ResourceManager.DrawableClass));
currentNavigationPage.Title = "Puzzle";
}
}
int IdFromTitle(string title, Type type)
{
string name = System.IO.Path.GetFileNameWithoutExtension(title);
int id = GetId(type, name);
return id;
}
int GetId(Type type, string memberName)
{
object value = type.GetFields().FirstOrDefault(p => p.Name == memberName)?.GetValue(type)
?? type.GetProperties().FirstOrDefault(p => p.Name == memberName)?.GetValue(type);
if (value is int)
return (int)value;
return 0;
}
}
}
I think you are using the custom render for tabbed page customization. For, Android you should refer the icon from Resource.Drawable. Please try with below code snippet in android renderer.
public class CustomTabRenderer: TabbedRenderer
{
private Activity _act;
protected override void OnModelChanged(VisualElement oldModel, VisualElement newModel)
{
base.OnModelChanged(oldModel, newModel);
_act = this.Context as Activity;
}
// You can do the below function anywhere.
public override void OnWindowFocusChanged(bool hasWindowFocus)
{
ActionBar actionBar = _act.ActionBar;
if (actionBar.TabCount > 0)
{
Android.App.ActionBar.Tab tabOne = actionBar.GetTabAt(0);
tabOne.SetIcon(Resource.Drawable.shell);
}
base.OnWindowFocusChanged(hasWindowFocus);
}
}
Refer this also: https://forums.xamarin.com/discussion/17654/tabbedpage-icons-not-visible-android
Try adding this code to OnElementChanged in TabbedPageRenderer
if (e.PropertyName == "Renderer")
{
ViewPager pager = (ViewPager)ViewGroup.GetChildAt(0);
TabLayout layout = (TabLayout)ViewGroup.GetChildAt(1);
for (int i = 0; i < layout.TabCount; i++)
{
var tab = layout.GetTabAt(i);
var icon = tab.Icon;
if (icon != null)
{
icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
Android.Support.V4.Graphics.Drawable.DrawableCompat.SetTintList(icon, colors);
}
}
}
More info here : https://montemagno.com/xamarin-forms-android-selected-and-unselected-tab-colors/
I'd like to have a UIPickerView in a Xamarin.iOS project. The UIPicker that I need, must be like this (hide by default and with toolbar and done button):
which is an example for Xamarin.forms !
I've seen already all questions on stack overflow and they are not in my case or they are not complete explanation for this purpose.
For demonstrating that I've tried already for create Done Toolbar, here is my code :
public class TestPickerViewController : UIViewController
{
PickerModel picker_model;
UIPickerView picker;
public TestPickerViewController()
{
Title = Texts.Home;
View.BackgroundColor = UIColor.White;
this.EdgesForExtendedLayout = UIRectEdge.None;
}
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();
List<Object> state_list = new List<Object>();
state_list.Add("1");
state_list.Add("2");
state_list.Add("3");
state_list.Add("4");
picker_model = new PickerModel(state_list);
picker = new UIPickerView();
picker.Model = picker_model;
picker.ShowSelectionIndicator = true;
UIToolbar toolbar = new UIToolbar();
toolbar.BarStyle = UIBarStyle.Black;
toolbar.Translucent = true;
toolbar.SizeToFit();
UIBarButtonItem doneButton = new UIBarButtonItem("Done", UIBarButtonItemStyle.Done, (s, e) =>
{
foreach (UIView view in this.View.Subviews)
{
if (view.IsFirstResponder)
{
UITextField textview = (UITextField)view;
textview.Text = picker_model.values[(int)picker.SelectedRowInComponent(0)].ToString();
textview.ResignFirstResponder();
}
}
});
toolbar.SetItems(new UIBarButtonItem[] { doneButton }, true);
View.AddSubviews(picker);
//How to add toolbar, action for opening toolbar and hide by default the list
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
View.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
View.AddConstraints(
picker.AtTopOf(View, 90),
picker.AtLeftOf(View, 50),
picker.WithSameWidth(View).Minus(100)
);
}
private void SetPicker(object sender, EventArgs e)
{
UITextField field = (UITextField)sender;
picker.Select(picker_model.values.IndexOf(field.Text), 0, true);
}
}
public class PickerModel : UIPickerViewModel
{
public IList<Object> values;
public event EventHandler<PickerChangedEventArgs> PickerChanged;
public PickerModel(IList<Object> values)
{
this.values = values;
}
public override nint GetComponentCount(UIPickerView picker)
{
return 1;
}
public override nint GetRowsInComponent(UIPickerView picker, nint component)
{
return values.Count;
}
public override string GetTitle(UIPickerView picker, nint row, nint component)
{
return values[(int)row].ToString();
}
public override nfloat GetRowHeight(UIPickerView picker, nint component)
{
return 40f;
}
public override void Selected(UIPickerView picker, nint row, nint component)
{
if (this.PickerChanged != null)
{
this.PickerChanged(this, new PickerChangedEventArgs { SelectedValue = values[(int)row] });
}
}
}
public class PickerChangedEventArgs : EventArgs
{
public object SelectedValue { get; set; }
}
I know that I have to add toolbar to somewhere which has done button. And I need also the action which hide the default Picker and show the list when we click on Select section and etc ...
Just assign the UIToolbar to the InputAccessoryView property of UITextField. Here's a code snippet for example:
UIToolbar toolBar = new UIToolbar(new CGRect(0, 0, 320, 44));
UIBarButtonItem flexibleSpaceLeft = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace,null,null);
UIBarButtonItem doneButton = new UIBarButtonItem("OK",UIBarButtonItemStyle.Done,this, new ObjCRuntime.Selector("DoneAction"));
UIBarButtonItem[] list = new UIBarButtonItem[] { flexibleSpaceLeft, doneButton };
toolBar.SetItems(list, false);
UIPickerView pickerView = new UIPickerView(new CGRect(0, 44, 320, 216));
pickerView.DataSource = new MyUIPickerViewDataSource();
pickerView.Delegate = new MyUIPickerViewDelegate();
pickerView.ShowSelectionIndicator = true;
//Assign the toolBar to InputAccessoryView
textField.InputAccessoryView = toolBar;
textField.InputView = pickerView;
And implement the Action like this:
[Export("DoneAction")]
private void DoneAction()
{
Console.WriteLine("Your Action!");
}
It works like this:
I'm still learning the ropes in Xamarin ios and have implemented a side drawer based on the following example Monotouch.SlideoutNavigation. In this tutorial,there's a main view controller class which then assigns a main navigation controller and a side menu.
The drawer menu options are fed into the menu class while the "home screen/first screen" is passed onto the main navigation controller class which is a subclass of a UINavigationController class.
My home screen is a tabcontroller class and I'm trying to make a reference to the navigation controller inside this class but it always returns null.
These are the two challenges I'm facing:
The navigation controller inside the tab controller and single tab view controllers is always null
The titles of my individual tab controller classes are not shown on the navigation bar.
Here's the AppDelegate class :
[Register ("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
public SlideoutNavigationController Menu { get; private set; }
public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
{
Menu = new SlideoutNavigationController ();
var tabBarController = GetViewController (Main, "MainTabBarController");
Menu.MainViewController = new MainNavigationController (tabBarController, Menu);
Menu.MenuViewController = new MenuNavigationController (new MenuControllerLeft (), Menu) { NavigationBarHidden = true };
SetRootViewController (Menu, false);
return true;
}
}
The MainTabController class
public partial class MainTabBarController : UITabBarController
{
UINavigationItem titleRequest,titleHome,titleSell;
public MainTabBarController (IntPtr handle) : base (handle)
{
//Create an instance of our AppDelegate
appDelegate = UIApplication.SharedApplication.Delegate as AppDelegate;
//Get an instance of our Main.Storyboard
var mainStoryboard = appDelegate.Main;
var tab1 = appDelegate.GetViewController (mainStoryboard, "Tab1");
var tab2 = appDelegate.GetViewController (mainStoryboard, "Tab2");
var tab3 = appDelegate.GetViewController (mainStoryboard, "Tab3");
var tabs = new UIViewController[] {
tab1, tab2, tab3
};
this.SelectedIndex = 1;
ViewControllers = tabs;
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
if(this.SelectedIndex == 0)
{
titleRequest = new UINavigationItem ("TAB 1");
this.NavigationController.NavigationBar.PushNavigationItem (titleRequest, true); // NavigationController here is null
}else if(this.SelectedIndex == 1)
{
titleHome = new UINavigationItem ("TAB 2");
this.NavigationController.NavigationBar.PushNavigationItem (titleHome, true);
}else{
titleSell = new UINavigationItem ("TAB 3");
this.NavigationController.NavigationBar.PushNavigationItem (titleSell, true);
}
}
}
The MainNavigation controller class
public class MainNavigationController : UINavigationController
{
public MainNavigationController(UIViewController rootViewController, SlideoutNavigationController slideoutNavigationController)
: this(rootViewController, slideoutNavigationController,
new UIBarButtonItem(UIImage.FromBundle("icon_sidemenu.png"), UIBarButtonItemStyle.Plain, (s, e) => {}))
{
}
public MainNavigationController(UIViewController rootViewController, SlideoutNavigationController slideoutNavigationController, UIBarButtonItem openMenuButton)
: base(rootViewController)
{
openMenuButton.Clicked += (s, e) => slideoutNavigationController.Open(true);
rootViewController.NavigationItem.LeftBarButtonItem = openMenuButton;
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
this.Delegate = new NavigationControllerDelegate();
InteractivePopGestureRecognizer.Enabled = true;
}
public override void PushViewController(UIViewController viewController, bool animated)
{
// To avoid corruption of the navigation stack during animations disabled the pop gesture
if (InteractivePopGestureRecognizer != null)
InteractivePopGestureRecognizer.Enabled = false;
base.PushViewController(viewController, animated);
}
private class NavigationControllerDelegate : UINavigationControllerDelegate
{
public override void DidShowViewController(UINavigationController navigationController, UIViewController viewController, bool animated)
{
// Enable the gesture after the view has been shown
navigationController.InteractivePopGestureRecognizer.Enabled = true;
}
}
}
Edit - Results after making changes suggested by Jason below
Could someone help me see what I'm doing wrong.
do this in AppDelegate:
tabs = new UITabBarController();
tabs.ViewControllers = new UIViewController[]{
new UINavigationController(new UIViewController() { Title = "Tab A" }),
new UINavigationController(new UIViewController() { Title = "Tab B" }),
new UINavigationController(new UIViewController() { Title = "Tab C" })
};
Menu = new SlideoutNavigationController();
Menu.MainViewController = new MainNavigationController(tabs, Menu);
Menu.MenuViewController = new MenuNavigationController(new DummyControllerLeft(), Menu) { NavigationBarHidden = true };
I finally found a work around this. For anyone using Dillan's solution and has a TabBarController class as one of the Menu classes, here's how I got it to work.
I wrapped the TabBarController class in a NavigationController,apart from the MainNavigationController class. I didn't have to wrap each tab in it's own NavigationController after this.That solves the null reference to the NavigationController inside the TabBarController class
To solve the titles being obscured inside each tab, I found a simple solution:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
try{
this.ViewControllerSelected += (object sender, UITabBarSelectionEventArgs e) => {
switch(TabBar.SelectedItem.Title)
{
case"TAB 1" :
Title = "TAB 1";
break;
case "TAB 2":
Title = "TAB 2";
break;
default:
Title = "TAB 3";
break;
}
};
}catch(Exception e)
{
Console.WriteLine (e.Message);
}
}
I have a requirement where I initially have a list of messages only ordered by date/time. The requirement if for the user to be able to click on a UISegmentedControl (list of 4 buttons) and be able to change the UITableView from a straight list to a grouped list (ie. grouped by category of message).
From what I've read, once the style is set on a UITableView you can not change it. So what is the best approach to satisfy this requirement? Kill the view and re-create with the appropriate style?
Not that it makes a huge difference, I am using Xamarin Studio and C#, targeting Mono 3.2.1 and iOS 6+
Rather than killing the view and re-instantiating, just maintain references to two UITableViews, one of each of the appropriate types. Toggle between them using your Controller class. The following simple example puts the toggling button in the same UIView as the table, which is probably not appropriate, but otherwise shows the technique:
public class ChangeableSource : UITableViewSource
{
public bool Grouped { get; set; }
public override int NumberOfSections(UITableView tableView)
{
if(Grouped)
{
return 4;
}
else
{
return 1;
}
}
public override int RowsInSection(UITableView tableview, int section)
{
return 3;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.DequeueReusableCell("Default");
if(cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, "Default");
}
cell.TextLabel.Text = String.Format("IndexPath {0} {1}", indexPath.Section, indexPath.Row);
return cell;
}
}
public class ToggleTableView : UIView
{
UITableView ungroupedView;
UITableView groupedView;
ChangeableSource changeableSource;
public void SetStyle(bool grouped)
{
changeableSource.Grouped = grouped;
if(changeableSource.Grouped)
{
ungroupedView.RemoveFromSuperview();
AddSubview(groupedView);
}
else
{
groupedView.RemoveFromSuperview();
AddSubview(ungroupedView);
}
}
public bool GetStyle()
{
return changeableSource.Grouped;
}
public ToggleTableView()
{
var btn = new UIButton(new RectangleF(10, 10, 150, 40));
btn.SetTitle("Change", UIControlState.Normal);
btn.TouchUpInside += (s,e) => ToggleStyle(this, new EventArgs());
var tvFrame = new RectangleF(0, 60, UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height - 60);
ungroupedView = new UITableView(tvFrame, UITableViewStyle.Plain);
groupedView = new UITableView(tvFrame, UITableViewStyle.Grouped);
AddSubview(btn);
AddSubview(ungroupedView);
changeableSource = new ChangeableSource();
changeableSource.Grouped = false;
ungroupedView.Source = changeableSource;
groupedView.Source = changeableSource;
}
public event EventHandler<EventArgs> ToggleStyle = delegate {};
}
public class TogglingTableController : UIViewController
{
public TogglingTableController() : base ()
{
}
public override void DidReceiveMemoryWarning()
{
// Releases the view if it doesn't have a superview.
base.DidReceiveMemoryWarning();
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var view = new ToggleTableView();
view.ToggleStyle += (s,e) =>
{
view.SetStyle(! view.GetStyle());
};
this.View = view;
}
}
[Register ("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
UIWindow window;
TogglingTableController viewController;
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
window = new UIWindow(UIScreen.MainScreen.Bounds);
viewController = new TogglingTableController();
window.RootViewController = viewController;
window.MakeKeyAndVisible();
return true;
}
}
public class Application
{
static void Main(string[] args)
{
UIApplication.Main(args, null, "AppDelegate");
}
}