Lately, I’ve been thinking a lot about search in mobile applications and realized just how hard it can be. There are quite of few things to consider when adding search. For example, performance, user interaction, and data, just to name a few. As such, I think I have some good topics to blog about in the future.
In the meantime, let’s start with figuring out how to use a native iOS search entry in the navigation bar of a Xamarin.Forms application. In order to complete this, we are going to create a custom Xamarin.Forms page. This way we can use a custom renderer on iOS to create a UISearchController
as needed. This page will inherit from ContentPage
and really just have a bunch of BindableProperty
s that we can use to interact with the UISearchBar
.
Here is what we are going to create:
Create a new class called
iOSSearchPage
which looks like this:public class iOSSearchPage : ContentPage { }
Add some
BindableProperty
s:public static readonly BindableProperty SearchTextProperty = BindableProperty.Create(nameof(SearchText), typeof(string), typeof(iOSSearchPage), string.Empty, BindingMode.TwoWay); public static readonly BindableProperty SearchCommandProperty = BindableProperty.Create(nameof(SearchCommand), typeof(ICommand), typeof(iOSSearchPage), null, BindingMode.OneWay); public static readonly BindableProperty SearchCommandParameterProperty = BindableProperty.Create(nameof(SearchCommand), typeof(object), typeof(iOSSearchPage), null, BindingMode.OneWay); public static readonly BindableProperty SearchCancelledCommandProperty = BindableProperty.Create(nameof(SearchCancelledCommand), typeof(ICommand), typeof(iOSSearchPage), null, BindingMode.OneWay); public static readonly BindableProperty SearchPlaceholderProperty = BindableProperty.Create(nameof(SearchPlaceholder), typeof(string), typeof(iOSSearchPage), string.Empty, BindingMode.OneWay); public static readonly BindableProperty IsSearchActiveProperty = BindableProperty.Create(nameof(IsSearchActive), typeof(bool), typeof(iOSSearchPage), false, BindingMode.OneWayToSource); public static readonly BindableProperty IsSearchFocusedProperty = BindableProperty.Create(nameof(IsSearchFocused), typeof(bool), typeof(iOSSearchPage), false, BindingMode.OneWayToSource); public static readonly BindableProperty ActionImageProperty = BindableProperty.Create(nameof(ActionImage), typeof(ImageSource), typeof(iOSSearchPage), null, BindingMode.OneWay); public static readonly BindableProperty ActionCommandProperty = BindableProperty.Create(nameof(ActionCommand), typeof(ICommand), typeof(iOSSearchPage), null, BindingMode.OneWay);
Many of the above properties are the same as what you would find on the standard
Xamarin.Forms
SearchBar
, with the following exceptions:1. `SearchCancelledCommand` - This binding will tell us when the user taps the "Cancel" button. 2. `ActionImage` & `ActionCommand` - Provides a mechanism to bind an image to a the `UISearchView`, using the [following API](https://developer.apple.com/documentation/uikit/uisearchbar/1624330-setimage), and a command to execute when tapped. I have used this to pop up a new view for a barcode scanner. Below is what that might look like:
Override
OnPropertyChanged
in theiOSSearchPage
class so that we can determine when a search is active. Basically, we want to know when the search control is focused, or if there is text in the search field. This will help us to determine when we should hide the main view, and replace it with the results view.protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) { base.OnPropertyChanged(propertyName); if (propertyName == SearchTextProperty.PropertyName || propertyName == IsSearchFocusedProperty.PropertyName) { if (this.IsSearchFocused) { this.IsSearchActive = true; } else if (!string.IsNullOrWhiteSpace(this.SearchText)) { this.IsSearchActive = true; } else { this.IsSearchActive = false; } } }
Create a new renderer in the iOS project called
SearchPageRenderer
, which inherits fromPageRenderer
and implementsIUISearchBarDelegate
.[assembly: ExportRenderer(typeof(iOSSearchPage), typeof(SearchPageRenderer))] namespace SearchApp.iOS.Components.Renderers { public class SearchPageRenderer : PageRenderer, IUISearchBarDelegate { }
Add a private field for a
UISearchController
to the custom renderer and instantiate it in the renderer’s constructor.private readonly UISearchController searchController; public SearchPageRenderer() { this.searchController = new UISearchController(searchResultsController: null) { HidesNavigationBarDuringPresentation = false, DimsBackgroundDuringPresentation = false, ObscuresBackgroundDuringPresentation = false, DefinesPresentationContext = true }; this.searchController.SearchBar.Delegate = this; // This is all for styling purposes, so you might have to play around with these values to make them fit your scenario. Or better yet, you could make them configurable properties in the Xamarin.Forms page. this.searchController.SearchBar.SearchBarStyle = UISearchBarStyle.Default; this.searchController.SearchBar.Translucent = true; this.searchController.SearchBar.BarStyle = UIBarStyle.Black; }
Override the
WillMoveToParentViewController
method in the renderer. This is what will add theUISearchController
to the navigation bar of the parent page. This is the important method. If you skip this step, you may never see the search bar in the navigation bar.public override void WillMoveToParentViewController(UIViewController parent) { parent.NavigationItem.SearchController = this.searchController; // This line is also for styling purposes. Do you want the UISearchBar to be visible when scrolling or not? parent.NavigationItem.HidesSearchBarWhenScrolling = false; }
If you run the solution, you should see the UISearchBar
in the navigation bar, but it will still be pretty useless at this point without the other plumbing we need to finish setting up.
Add the following methods to the renderer class. These will wire up the events from our native iOS control to our custom Xamarin.Forms
iOSSearchPage
. These methods are actually part of theIUISearchBarDelegate
class that this renderer implements, and we use the[Export]
attributes in order to “subscribe” to the events in iOS.[Export("searchBarCancelButtonClicked:")] public void CancelButtonClicked(UISearchBar searchBar) { if (this.Element is iOSSearchPage iosSearchPage && iosSearchPage.SearchCancelledCommand != null && iosSearchPage.SearchCancelledCommand.CanExecute(null)) { this.TextChanged(this.searchController.SearchBar, string.Empty); iosSearchPage.SearchCancelledCommand.Execute(null); } } [Export("searchBarTextDidBeginEditing:")] public void OnEditingStarted(UISearchBar searchBar) { if (this.Element is iOSSearchPage iosSearchPage) { iosSearchPage.IsSearchFocused = true; } } [Export("searchBarTextDidEndEditing:")] public void OnEditingStopped(UISearchBar searchBar) { if (this.Element is iOSSearchPage iosSearchPage) { iosSearchPage.IsSearchFocused = false; } } [Export("searchBarSearchButtonClicked:")] public void SearchButtonClicked(UISearchBar searchBar) { if (this.Element is iOSSearchPage iosSearchPage && iosSearchPage.SearchCommand != null && iosSearchPage.SearchCommand.CanExecute(this.searchController.SearchBar.Text)) { iosSearchPage.SearchCommand.Execute(this.searchController.SearchBar.Text); } } [Export("searchBarBookmarkButtonClicked:")] public void BookmarkButtonClicked(UISearchBar searchBar) { if (this.Element is iOSSearchPage iosSearchPage && iosSearchPage.ActionCommand != null && iosSearchPage.ActionCommand.CanExecute(null)) { iosSearchPage.ActionCommand.Execute(null); } } [Export("searchBar:textDidChange:")] public void TextChanged(UISearchBar searchBar, string searchText) { if (this.Element is iOSSearchPage iosSearchPage) { iosSearchPage.SetValue(iOSSearchPage.SearchTextProperty, searchText); } }
Notice the above methods are really just determining when to change fields on our custom
iOSSearchPage
. While this stuff is pretty simple, it can take a while to find these things in the native API.There is one last method to override:
OnElementChanged
. We use this method to set theUISearchBar
placeholder andActionImage
when the element is set. So basically, after the renderer is created by the underlyingXamarin.Forms
framework, and anElement
(aXamarin.Forms
control) is assigned, we want to update some values on the native iOS control.protected override void OnElementChanged(VisualElementChangedEventArgs e) { base.OnElementChanged(e); if (e.NewElement is iOSSearchPage iosSearchPage) { this.searchController.SearchBar.Placeholder = iosSearchPage.SearchPlaceholder; Task.Run(async () => { if (iosSearchPage.ActionImage != null) { try { var handler = new FileImageSourceHandler(); var actionImage = await handler.LoadImageAsync(iosSearchPage.ActionImage); this.searchController.SearchBar.SetImageforSearchBarIcon(actionImage, UISearchBarIcon.Bookmark, UIControlState.Normal); this.searchController.SearchBar.ShowsBookmarkButton = true; } catch (Exception ex) { Debug.WriteLine($"Error loading ActionImage: {ex.Message}"); } } }); } }
And that’s it for the custom control. Now to use it, we can create a page like the following:
<?xml version="1.0" encoding="utf-8"?>
<pages:iOSSearchPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SearchApp"
xmlns:pages="clr-namespace:SearchApp.Components.Pages"
x:Class="SearchApp.MainPage"
Title="Main"
ActionImage="barcode_black_24">
<StackLayout>
<Label
Text="Welcome to Xamarin.Forms!"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>
</pages:iOSSearchPage>
The above example doesn’t have all of the properties we created, but they can easily be added. In fact, I hope to build on this example in my upcoming blog posts, but in the meantime, here is a working GitHub example.