November 17, 2015 · Swift

iOS Search Bar Animation with Swift

Search is an important part of many iOS apps. Using an API, such as UISearchController is easy, but it makes it harder to customize the appearance of the user interface. In this tutorial I'll explain how to make an animated UISearchBar in Swift which slides up and expands. I'll show how to add a search bar to a view controller, animate its position and display results. I used this animated search bar in Voyageur. I will cover the code used in this sample app. Here's what this looks like in Voyageur:

Add a search bar and table view

In order to display search results you need a table view and a data source. I like to put view-related code in its own class, so I'll create MainView.swift and add a UISearchBar and UITableView as subviews. Changing the appearance of the search bar is difficult, so I actually used a button in place of the search bar when it's not active. I use PureLayout (installed with Cocoapods) to create auto layout constraints. These constraints will be animated later. This is how MainView should look initially:

import UIKit  
import PureLayout

class MainView: UIView {

    private var searchBar: UISearchBar!
    private var searchButton: UIButton!
    private var resultsTable: UITableView!

    private let searchButtonHeight: CGFloat = 60
    private let searchButtonWidth: CGFloat = 200

    private let searchBarStartingAlpha: CGFloat = 0
    private let searchButtonStartingAlpha: CGFloat = 1
    private let tableStartingAlpha: CGFloat = 0
    private let searchBarEndingAlpha: CGFloat = 1
    private let searchButtonEndingAlpha: CGFloat = 0
    private let tableEndingAlpha: CGFloat = 1

    private let searchButtonStartingCornerRadius: CGFloat = 20
    private let searchButtonEndingCornerRadius: CGFloat = 0

    private var didSetupConstraints = false

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupViews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    // MARK: - Initialization

    func setupViews() {
        setupSearchBar()
        setupSearchButton()
        setupResultsTable()
    }

    func setupSearchBar() {
        searchBar = UISearchBar.newAutoLayoutView()
        searchBar.showsCancelButton = true
        searchBar.alpha = searchBarStartingAlpha
        addSubview(searchBar)
    }

    func setupSearchButton() {
        searchButton = UIButton(type: .Custom)
        searchButton.translatesAutoresizingMaskIntoConstraints = false
        searchButton.addTarget(self, action: "searchClicked:", forControlEvents: .TouchUpInside)
        searchButton.setTitle("Search", forState: .Normal)
        searchButton.backgroundColor = .blueColor()
        searchButton.layer.cornerRadius = searchButtonStartingCornerRadius
        addSubview(searchButton)
    }

    func setupResultsTable() {
        resultsTable = UITableView.newAutoLayoutView()
        resultsTable.alpha = tableStartingAlpha
        addSubview(resultsTable)
    }

    // MARK: - Layout

    override func updateConstraints() {
        if !didSetupConstraints {
            searchBar.autoAlignAxisToSuperviewAxis(.Vertical)
            searchBar.autoMatchDimension(.Width, toDimension: .Width, ofView: self)
            searchBar.autoPinEdgeToSuperviewEdge(.Top)

            searchButton.autoSetDimension(.Height, toSize: searchButtonHeight)
            searchButton.autoSetDimension(.Width, toSize: searchButtonWidth)
            searchButton.autoCenterInSuperview()

            resultsTable.autoAlignAxisToSuperviewAxis(.Vertical)
            resultsTable.autoPinEdgeToSuperviewEdge(.Leading)
            resultsTable.autoPinEdgeToSuperviewEdge(.Trailing)
            resultsTable.autoPinEdgeToSuperviewEdge(.Bottom)
            resultsTable.autoPinEdge(.Top, toEdge: .Bottom, ofView: searchBar)

            didSetupConstraints = true
        }

        super.updateConstraints()
    }

    // MARK: - User Interaction

    func searchClicked(sender: UIButton!) {
    }
}

This view needs to be added as a subview of your view controller. I also embedded the view controller inside a navigation controller. ViewController.swift looks like this:

import UIKit  
import PureLayout

class ViewController: UIViewController {

    private var mainView: MainView!
    private var didSetupConstraints = false

    // MARK: - View Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        mainView = MainView.newAutoLayoutView()
        view.addSubview(mainView)
    }

    // MARK: - Layout

    override func updateViewConstraints() {
        if !didSetupConstraints {
            mainView.autoPinToTopLayoutGuideOfViewController(self, withInset: 0)
            mainView.autoPinToBottomLayoutGuideOfViewController(self, withInset: 0)
            mainView.autoPinEdgeToSuperviewEdge(.Leading)
            mainView.autoPinEdgeToSuperviewEdge(.Trailing)
            didSetupConstraints = true
        }

        super.updateViewConstraints()
    }
}

The view will initially look like this:

A button is centered in the view and the search bar and results table are hidden. At the moment, tapping the button does nothing. The button's functionality will be implemented in searchClicked.

Animate search bar

The next step is to animate the subviews. In this tutorial I'll show how to slide the search button upwards, which gives the impression that the search bar is expanding. Clicking the search button triggers an animateWithDuration block. The search bar's layout constraints need to be updated in order to change its position, and this is triggered by calling setNeedsUpdateConstraints() and updateConstraintsIfNeeded(). First, to MainView I added a variable to keep track of the search bar position:

private var searchBarTop = false  

Then I added a helper function showSearchBar which animates the search button, search bar and results table. This function is called in searchClicked.

// MARK: - User Interaction

func searchClicked(sender: UIButton!) {  
    showSearchBar(searchBar)
}

// MARK: - Helpers

func showSearchBar(searchBar: UISearchBar) {  
    searchBarTop = true

    setNeedsUpdateConstraints()
    updateConstraintsIfNeeded()

    UIView.animateWithDuration(0.3,
        animations: {
            searchBar.becomeFirstResponder()
            self.layoutIfNeeded()
        }, completion: { finished in
            UIView.animateWithDuration(0.2,
                animations: {
                    searchBar.alpha = self.searchBarEndingAlpha
                    self.resultsTable.alpha = self.tableEndingAlpha
                    self.searchButton.alpha = self.searchButtonEndingAlpha
                    self.searchButton.layer.cornerRadius = self.searchButtonEndingCornerRadius
                }
            )
        }
    )
}

This doesn't change the position of the search button yet. You need to add to updateConstraints. First add the following variables:

private var searchButtonWidthConstraint: NSLayoutConstraint?  
private var searchButtonEdgeConstraint: NSLayoutConstraint?  

Then update updateConstraints:

override func updateConstraints() {  
    if !didSetupConstraints {
        searchBar.autoAlignAxisToSuperviewAxis(.Vertical)
        searchBar.autoMatchDimension(.Width, toDimension: .Width, ofView: self)
        searchBar.autoPinEdgeToSuperviewEdge(.Top)

        searchButton.autoSetDimension(.Height, toSize: searchButtonHeight)
        searchButton.autoAlignAxisToSuperviewAxis(.Vertical)

        resultsTable.autoAlignAxisToSuperviewAxis(.Vertical)
        resultsTable.autoPinEdgeToSuperviewEdge(.Leading)
        resultsTable.autoPinEdgeToSuperviewEdge(.Trailing)
        resultsTable.autoPinEdgeToSuperviewEdge(.Bottom)
        resultsTable.autoPinEdge(.Top, toEdge: .Bottom, ofView: searchBar)

        didSetupConstraints = true
    }

    searchButtonWidthConstraint?.autoRemove()
    searchButtonEdgeConstraint?.autoRemove()

    if searchBarTop {
        searchButtonWidthConstraint = searchButton.autoMatchDimension(.Width, toDimension: .Width, ofView: self)
        searchButtonEdgeConstraint = searchButton.autoPinEdgeToSuperviewEdge(.Top)
    } else {
        searchButtonWidthConstraint = searchButton.autoSetDimension(.Width, toSize: searchButtonWidth)
        searchButtonEdgeConstraint = searchButton.autoAlignAxisToSuperviewAxis(.Horizontal)
    }

    super.updateConstraints()
}

The position of the search button is animated by changing searchButtonWidthConstraint and searchButtonEdgeConstraint.

Now when the search button is clicked it slides up and expands, but there's still one problem. The search bar can't be dismissed.

Dismiss search bar

This is pretty easy to do. All you need is a helper function, which reverses the steps in showSearchBar. I called it dismissSearchBar:

func dismissSearchBar(searchBar: UISearchBar) {  
    searchBarTop = false

    UIView.animateWithDuration(0.2,
        animations: {
            searchBar.alpha = self.searchBarStartingAlpha
            self.resultsTable.alpha = self.tableStartingAlpha
            self.searchButton.alpha = self.searchButtonStartingAlpha
            self.searchButton.layer.cornerRadius = self.searchButtonStartingCornerRadius
        }, completion:  { finished in
            self.setNeedsUpdateConstraints()
            self.updateConstraintsIfNeeded()
            UIView.animateWithDuration(0.3,
                animations: {
                    searchBar.resignFirstResponder()
                    self.layoutIfNeeded()
                }
            )
        }
    )
}

This function should be called when the user taps the cancel button on the search bar. To do this MainView needs to conform to UISearchBarDelegate. In setupSearchBar add:

searchBar.delegate = self  

In an extension at the end of MainView.swift add:

extension MainView: UISearchBarDelegate {  
    func searchBarCancelButtonClicked(searchBar: UISearchBar) {
        searchBar.text = ""
        dismissSearchBar(searchBar)
    }
}

Now you should have a search bar that animates up when pressed and animates back down when dismissed. Here is the code used in this tutorial. The next step is to search data from the table. This is how the final view should look:

Comments powered by Disqus