Adding a Search Bar to Xamarin.Forms.NavigationPage
31 Oct 2020 Update: Updated for AndroidX + Xamarin.Essentials
Let's look at how to add the native search bar control into the Xamarin.Forms.NavigationPage
Both Xamarin.iOS and Xamarin.Android offer the capability to natively add a search bar to the navigation bar using UISearchController
and SearchView
, respectively.
But, it's not simple in Xamarin.Forms as one might expect.
If you'd like to see a completed solution, check out this app: https://github.com/brminnick/GitTrends
Otherwise, let's jump to the steps & code following this lovely GIF:
iOS | Android |
---|---|
Xamarin.Forms Project
In the Xamarin.Forms project, we'll want to do the following:
- Use a Xamarin.Forms Platform Specific to use Large Titles on iOS
- Create an interface,
ISearchPage
- In our
Xamarin.Forms.ContentPage
, implementISearchPage
iOS Large Titles
In iOS 11, Apple introduced prefersLargeTitles and updated their Human Interface Guidelines, recommending that iOS apps use Large Titles.
This is easy to accomplish in Xamarin.Forms thanks to the platform-specific method Xamarin.Forms.PlatformConfiguration.iOSSpecific.NavigationPage.SetPreferesLargeTitles
.
We can set this platform-specific in our App.cs
class:
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
public class App : Xamarin.Forms.Application
{
public App()
{
var navigationPage = new Xamarin.Forms.NavigationPage(new MyContentPage());
navigationPage.On<iOS>().SetPrefersLargeTitles(true);
MainPage = navigationPage;
}
}
ISearchPage
Let's create an interface, ISearchPage
, that can be used across the Xamarin.Forms, Xamarin.Android and Xamarin.iOS projects.
public interface ISearchPage
{
void OnSearchBarTextChanged(string text);
event EventHandler<string> SearchBarTextChanged;
}
Xamarin.Forms.ContentPage
Let's now implement ISearchPage
in our Xamarin.Forms.ContentPage
public class MyContentPage : ContentPage, ISearchPage
{
public MyContentPage()
{
SearchBarTextChanged += HandleSearchBarTextChanged;
}
public event EventHandler<string> SearchBarTextChanged;
void ISearchPage.OnSearchBarTextChanged((string text) => SearchBarTextChanged?.Invoke(this, text);
void HandleSearchBarTextChanged(object sender, string searchBarText)
{
//Logic to handle updated search bar text
}
}
Xamarin.iOS Project
In the Xamarin.iOS project, we'll be doing the following:
- Create
SearchPageRenderer
Our Xamarin.iOS Custom Renderer uses a PageRenderer
that adds a UISearchController
to the NavigationItem
on its parent page which is a UINavigationController
. (The parent page is a UINavigationController
, because in App.cs
, we used a Xamarin.Forms.NavigationPage
.)
If you're new to Xamarin.iOS and/or Custom Renderers, here's a chart showing how the various UI controls relate to each other on iOS:
Xamarin.iOS | Xamarin.Forms Renderer | Xamarin.Forms |
---|---|---|
UINavigationController |
NavigationRenderer |
NavigationPage |
UIViewController |
PageRenderer |
ContentPage |
using System;
using UIKit;
using MyNamespace;
using MyNamespace.iOS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
namespace MyNamespace.iOS
{
public class SearchPageRenderer : PageRenderer, IUISearchResultsUpdating
{
readonly UISearchController _searchController;
public SearchPageRenderer()
{
_searchController = new UISearchController(searchResultsController: null)
{
SearchResultsUpdater = this,
DimsBackgroundDuringPresentation = false,
HidesNavigationBarDuringPresentation = false,
HidesBottomBarWhenPushed = true
};
_searchController.SearchBar.Placeholder = string.Empty;
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
if (ParentViewController.NavigationItem.SearchController is null)
{
ParentViewController.NavigationItem.SearchController = _searchController;
DefinesPresentationContext = true;
//Work-around to ensure the SearchController appears when the page first appears https://stackoverflow.com/a/46313164/5953643
ParentViewController.NavigationItem.SearchController.Active = true;
ParentViewController.NavigationItem.SearchController.Active = false;
}
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
ParentViewController.NavigationItem.SearchController = null;
}
public void UpdateSearchResultsForSearchController(UISearchController searchController)
{
if (Element is ISearchPage searchPage)
searchPage.OnSearchBarTextChanged(searchController.SearchBar.Text);
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
e.NewElement.SizeChanged += HandleSizeChanged;
}
//Work-around to accommodate UISearchController height, https://github.com/brminnick/GitTrends/issues/171
void HandleSizeChanged(object sender, EventArgs e)
{
if (ParentViewController?.NavigationItem.SearchController != null
&& Element.Height > -1
&& Element is Page page)
{
Element.SizeChanged -= HandleSizeChanged;
if (NavigationController.NavigationBar.PrefersLargeTitles is true)
{
var statusBarSize = UIApplication.SharedApplication.StatusBarFrame.Size;
var statusBarHeight = Math.Min(statusBarSize.Height, statusBarSize.Width);
page.Padding = new Thickness(page.Padding.Left,
page.Padding.Top,
page.Padding.Right,
page.Padding.Bottom + statusBarHeight);
}
}
}
}
}
Note: If you'd like to add a search bar to multiple pages, you can add additional ExportRenderer
assembly attributes like so:
[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
[assembly: ExportRenderer(typeof(MyContentPage2), typeof(SearchPageRenderer))]
Xamarin.Android Project
In the Xamarin.Android project, we'll be doing the following:
- Create
Resources > menu > MainMenu.xml
- Create
SearchPageRenderer
MainMenu.xml
In the Xamarin.Android project, we'll first need to add MainMenu.xml
to Resources > menu
.
1. In the Xamarin.Android project, in the Resources
folder, create a new folder called menu
(if one doesn't already exist)
- Note: The folder name,
menu
, needs to be lowercase
2. In the Resources > menu
folder, create a new file called MainMenu.xml
3. Open Resources > menu > MainMenu.xml
4. In MainMenu.xml
, add the following code:
<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/ActionSearch"
android:title="Filter"
android:icon="@android:drawable/ic_menu_search"
app:showAsAction="always|collapseActionView"
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
</menu>
Xamarin.Android Custom Renderer
Our Xamarin.Android Custom Renderer uses a PageRenderer
to add a SearchView
to the Toolbar
.
- Note: Our custom renderer requires the Xamarin.Essentials NuGet Package. Make sure to add the NuGet package to your Xamarin.Android project first and follow its instructions to initialize it in the
MainApplication
.
using System.Collections.Generic;
using System.Linq;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using AndroidX.AppCompat.Widget;
using MyNamespace;
using MyNamespace.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
namespace MyNamespace.Droid
{
public class SearchPageRenderer : PageRenderer
{
public SearchPageRenderer(Context context) : base(context)
{
}
//Add the Searchbar once Xamarin.Forms creates the Page
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
if (e.NewElement is ISearchPage && e.NewElement is Page page && page.Parent is NavigationPage navigationPage && navigationPage.CurrentPage is ISearchPage)
AddSearchToToolbar(page.Title);
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
if (Element is ISearchPage && Element is Page page && page.Parent is NavigationPage navigationPage)
{
//Workaround to re-add the SearchView when navigating back to an ISearchPage, because Xamarin.Forms automatically removes it
navigationPage.Popped += HandleNavigationPagePopped;
navigationPage.PoppedToRoot += HandleNavigationPagePopped;
}
}
//Adding the SearchBar in OnSizeChanged ensures the SearchBar is re-added after the device is rotated, because Xamarin.Forms automatically removes it
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
base.OnSizeChanged(w, h, oldw, oldh);
if (Element is ISearchPage && Element is Page page && page.Parent is NavigationPage navigationPage && navigationPage.CurrentPage is ISearchPage)
{
AddSearchToToolbar(page.Title);
}
}
protected override void Dispose(bool disposing)
{
if (GetToolbar() is Toolbar toolBar)
toolBar.Menu?.RemoveItem(Resource.Menu.MainMenu);
base.Dispose(disposing);
}
static IEnumerable<Toolbar> GetToolbars(ViewGroup viewGroup)
{
for (int i = 0; i < viewGroup.ChildCount; i++)
{
if (viewGroup.GetChildAt(i) is Toolbar toolbar)
{
yield return toolbar;
}
else if (viewGroup.GetChildAt(i) is ViewGroup childViewGroup)
{
foreach (var childToolbar in GetToolbars(childViewGroup))
yield return childToolbar;
}
}
}
Toolbar? GetToolbar()
{
if (Xamarin.Essentials.Platform.CurrentActivity.Window?.DecorView.RootView is ViewGroup viewGroup)
{
var toolbars = GetToolbars(viewGroup);
//Return top-most Toolbar
return toolbars.LastOrDefault();
}
return null;
}
//Workaround to re-add the SearchView when navigating back to an ISearchPage, because Xamarin.Forms automatically removes it
void HandleNavigationPagePopped(object sender, NavigationEventArgs e)
{
if (sender is NavigationPage navigationPage
&& navigationPage.CurrentPage is ISearchPage)
{
AddSearchToToolbar(navigationPage.CurrentPage.Title);
}
}
void AddSearchToToolbar(string pageTitle)
{
if (GetToolbar() is Toolbar toolBar
&& toolBar.Menu?.FindItem(Resource.Id.ActionSearch)?.ActionView?.JavaCast<SearchView>()?.GetType() != typeof(SearchView))
{
toolBar.Title = pageTitle;
toolBar.InflateMenu(Resource.Menu.MainMenu);
if (toolBar.Menu?.FindItem(Resource.Id.ActionSearch)?.ActionView?.JavaCast<SearchView>() is SearchView searchView)
{
searchView.QueryTextChange += HandleQueryTextChange;
searchView.ImeOptions = (int)ImeAction.Search;
searchView.InputType = (int)InputTypes.TextVariationFilter;
searchView.MaxWidth = int.MaxValue; //Set to full width - http://stackoverflow.com/questions/31456102/searchview-doesnt-expand-full-width
}
}
}
void HandleQueryTextChange(object sender, SearchView.QueryTextChangeEventArgs e)
{
if (Element is ISearchPage searchPage)
searchPage.OnSearchBarTextChanged(e?.NewText ?? string.Empty);
}
}
}
Note: If you'd like to add a search bar to multiple pages, you can add additional ExportRenderer
assembly attributes like so:
[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
[assembly: ExportRenderer(typeof(MyContentPage2), typeof(SearchPageRenderer))]