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 BindablePropertys that we can use to interact with the UISearchBar.

Here is what we are going to create:

device-iOSSearchBar
  1. Create a new class called iOSSearchPage which looks like this:

    public class iOSSearchPage : ContentPage { }
    
  2. Add some BindablePropertys:

    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:
    

    device-action-image

  3. Override OnPropertyChanged in the iOSSearchPage 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;
            }
        }
    }
    
  4. Create a new renderer in the iOS project called SearchPageRenderer, which inherits from PageRenderer and implements IUISearchBarDelegate.

    [assembly: ExportRenderer(typeof(iOSSearchPage), typeof(SearchPageRenderer))]
    namespace SearchApp.iOS.Components.Renderers
    {
        public class SearchPageRenderer : PageRenderer, IUISearchBarDelegate { }
    
  5. 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;
    }
    
  6. Override the WillMoveToParentViewController method in the renderer. This is what will add the UISearchController 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.

  1. 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 the IUISearchBarDelegate 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.

  2. There is one last method to override: OnElementChanged. We use this method to set the UISearchBar placeholder and ActionImage when the element is set. So basically, after the renderer is created by the underlying Xamarin.Forms framework, and an Element (a Xamarin.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.

@jtaubensee