0

I'm trying to create a search bar that appears on top of the nav bar when you click on "Search" (which is also in the nav bar). Clicking on a search result will take you to a Detail page where the Search Bar will disappear. When you click to go back to the Home VC, the search bar reappears. Thought it would be simple but encountering a few issues. This is what I've done so far

To create the search bar when you tap "Search" in the HomeVC I did this:

@IBAction func SearchAction(sender: AnyObject) {
        searchController.searchResultsUpdater = self
        searchController.searchBar.delegate = self
        definesPresentationContext = true
        searchController.dimsBackgroundDuringPresentation = false
        searchController.hidesNavigationBarDuringPresentation = false
        self.navigationController?.presentViewController(searchController, animated: true, completion: nil)
}

1st issue, when you go to the Detail page, it search bar doesn't disappear.

Solution: In PrepareForSegue, I added

self.searchController.searchBar.removeFromSuperview()

and replaced the unwindSegue with a delegate so that when the user goes back to Home VC from Detail page, the search bar is added back in. The HomeVC performs the following:

func didDismissDetail() {
    self.navigationController?.popViewControllerAnimated(true)
    if searchController.active == true {
        self.navigationController?.view.addSubview(searchController.searchBar)
    }
}

2nd issue: There's a bug in the scenario where you search, tap to go to the Detail page, go back to Home VC, tap Cancel to close the Search controller. When you click on Search button in the nav bar again, nothing happens. I get this error: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally an active controller .' Solution: I added this

func searchBarCancelButtonClicked(searchBar: UISearchBar) {
    self.searchController.searchBar.removeFromSuperview()        
}

This works however, there is no animation when the view is removed. Sorry, I know it's a little long winded - my question is:

  1. Is there a better way to create this user experience?
  2. If not, how can you animate the removal of the search bar when the user taps on Cancel

    Here are some images:

Home ScreenAfter clicking "Search"

Danny
  • 190
  • 1
  • 12

2 Answers2

3

Instead of using self.searchController.searchBar.removeFromSuperview() you should use:

navigationController?.dismissViewControllerAnimated(true, completion: nil)`

to dismiss the view. Also, you should give your segue an identifier and check for it in prepareForSegue. You implementation may look something like this:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
   if segue.identifier == "ToDetail" {
      let destination = segue.destinationViewController as! DetailViewController
      navigationController?.dismissViewControllerAnimated(true, completion: nil)
     //Pass Info to Detail View
   }
}

and then in didSelectRowAtIndexPath just perform your segue with performSegueWithIdentifier.

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
   performSegueWithIdentifier("ToDetail", sender: self)
}

To allow the searchBar to reappear you can set up a boolean variable called searchBarShouldBeOpen on the main view controller. In your SearchAction function set this value to true, and then in viewWillAppear use an if statement with searchBarShouldBeOpen to decide whether to run SearchAction. Also, consider making SearchAction a regular function and moving everything but the last line shown in your example to viewDidLoad. Lastly, implement UISearchBarDelegate for your Main View Controller and override searchBarCancelButtonClicked and inside it set searchBarShouldBeOpen to false. Your Main View Controller would look something like this:

class ViewController: UIViewController {
 //...Your Variables

 var searchBarShouldBeOpen: Bool = false

 let searchBarKey = "SearchBarText"

 override func viewDidLoad() {
    super.viewDidLoad()
    searchController.searchResultsUpdater = self
    searchController.dimsBackgroundDuringPresentation = false
    definesPresentationContext = true
    searchController.searchBar.delegate = self
    searchController.hidesNavigationBarDuringPresentation = false

    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Search", style: .Plain, target: self, action: #selector(ViewController.showSearchBar))
  }

  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    if searchBarShouldBeOpen {
       navigationController?.presentViewController(searchController, animated: false, completion: nil)
       searchBarShouldBeOpen = true
       if let previousText = NSUserDefaults.standardUserDefaults().stringForKey(searchBarKey) {
          searchController.searchBar.text = previousText
       }
    }  
  }

  func showSearchBar() {
    navigationController?.presentViewController(searchController, animated: true, completion: nil)
     searchBarShouldBeOpen = true
  }

  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "ToDetail" {
      let destination = segue.destinationViewController as! DetailViewController
      if searchController.searchBar.text != "" { 
         NSUserDefaults.standardUserDefaults().setValue(searchController.searchBar.text, forKey: searchBarKey)
      }
       //Pass Info
    }
  }
  //Your functions...
}

extension ViewController: UISearchBarDelegate {
   func searchBarCancelButtonClicked(searchBar: UISearchBar) {
     searchBarShouldBeOpen = false
  }
}

In your Detail View Controller add this:

override func viewWillAppear(animated: Bool) {
   super.viewWillAppear(animated)
   navigationController?.dismissViewControllerAnimated(true, completion: nil)
}

Edits:

  1. Changed viewWillAppear to manually present the UISearchBar without animation rather than using showSearchBar so that it would not animate every time.

  2. Added missing call to super.viewWillAppear(animated)

  3. Moved the dismissal of the UISearchBar to the viewWillAppear of the detail view Controller so the search doesn't go away before the segue.

  4. Persisted the UISearchBar's text using NSUserDefaults (in prepareForSegue) so that way it could be loaded in the viewWillAppear of the Main View Controller to set the search to what it previously was.

  5. Added the searchBarKey constant to use as the key for the UISearchBar text persistence, so a "Magic String" would not be used to set and access the saved text.

Note: You should really consider using didSelectRowAtIndexPath to perform the segue.

Other Option: If you want the UISearchBar to animate out simultaneously with the transitioning of the Main View Controller to the Detail View Controller, you can use a custom segue (this will make the animation a bit smoother). I have made a custom segue example that creates this effect. Note that you will have to change the segue between the view controllers in Interface Builder to have kind "Custom" and be the following subclass of UIStoryboardSegue.

class CustomSegue: UIStoryboardSegue {

   override func perform() {
      let pushOperation = NSBlockOperation()
      pushOperation.addExecutionBlock {
         self.sourceViewController.navigationController?.pushViewController(self.destinationViewController, animated: true)
      }
      pushOperation.addExecutionBlock {
        self.sourceViewController.navigationController?.dismissViewControllerAnimated(true, completion: nil)
      }
      pushOperation.queuePriority = .Normal
      pushOperation.qualityOfService = .UserInitiated
      let operationQueue = NSOperationQueue.mainQueue()
      operationQueue.addOperation(pushOperation)

   }

}

You can read more about the code in the custom segue here at the following links:

  1. Custom Segues: http://www.appcoda.com/custom-segue-animations/
  2. NSOperation: http://nshipster.com/nsoperation/

Hope this helps! If you need elaboration just ask! :)

Ike10
  • 1,585
  • 12
  • 14
  • Thanks for the response Ike10. I tried it but the issue with dismiss is that it deactivates the search controller so when you go back to the Home VC from Detail, the search bar doesn't appear again. If you add a present VC to the delegate method when it lands on Home VC, there's no way to differentiate if the search controller was active beforehand – Danny Jun 23 '16 at 01:22
  • Awesome @Ike10! Yeah this would work. I've implemented this and ran into two further minor issues: 1) When you go from HomeVC to Detail, you see it go from the search results back to the original list before segueing into Detail. FYI I didn't use didSelectRowAtIndexPath. I used 'let indexPath = tableView.indexPathForSelectedRow' in the cellForRowAtIndexPath instead. Should be the same – Danny Jun 23 '16 at 10:31
  • 2) When you go back to the search results from Detail, it always get animated. You should just see the search results (the last page you saw). I created a 2nd method with animation for dismiss off and ask viewWillAppear to call this instead if searchBarShouldBeOpen = true. This didn't work and don't know why. – Danny Jun 23 '16 at 10:38
  • @Danny See my edits for a possible solution to your issues. – Ike10 Jun 23 '16 at 14:55
  • @Danny added something for a cleaner transition from HomeVC to DetailVC – Ike10 Jun 23 '16 at 16:19
  • Thanks for the advice on this. I can't seem to get it working on mine and not sure why. Think I'm just going to keep my existing solution for now. Also, why is it better to use didSelectRowAtIndexPath rather than just tableview.indexPathForSelectedRow in the PrepareForSegue? – Danny Jun 24 '16 at 12:56
  • @Danny , which part is not working? And `didSelectRowAtIndexPath` and `performSegueWithIdentifier` is better because `indexPathForSelectedRow` in `prepareForSegue` because `prepareForSegue` is called at every segue so the code becomes less efficient as well as the fact that `indexPathForSelectedRow` returns an optional which creates the need to create an unnecessary if let binding to unwrap it. (It is generally a good idea to make code a clean and concise as possible for maintainability reasons). – Ike10 Jun 24 '16 at 14:05
  • very good point! Also I got it working now! What would the code in prepareForSegue look like for passing the data to the Detail VC? – Danny Jun 24 '16 at 15:19
  • @Danny I'm glad you got it working! Check out this question's answer for a good explanation of that: http://stackoverflow.com/questions/26207846/pass-data-through-segue – Ike10 Jun 24 '16 at 15:56
0

For issue #1, instead of self.searchController.searchBar.removeFromSuperview(), you could do self.searchController.isActive = false in prepare(for:sender:). That will deactive the search bar when you unwind.

yohannes
  • 1,022
  • 11
  • 13