Subviews in presented ViewController gets wrong position after rotation

Viewed 610

I'm experimenting with UISearchController, but I can't get it right. I present a clean UISearchController from my own UIViewController, and it looks great - but as soon as I rotate the device, it gets shifted a few points up or down.

To recreate this, just do these few steps:

  1. Create a new Single View project
  2. Delete Main.storyboard from the project files and remove its name from the project settings (Project -> General -> Target -> Main Interface)

In AppDelegate.swift:

application didFinishLaunchingWithOptions...{
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = ViewController()
    window?.makeKeyAndVisible()
    return true
}

ViewController.swift:

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        let button = UIButton(frame: CGRect(x: 20, y: 200, width: 100, height: 40))
        button.setTitle("Search", for: .normal)
        button.backgroundColor = .black
        button.addTarget(self, action: #selector(click), for: .touchUpInside)
        self.view.addSubview(button)
    }
    @objc func click(){
        self.present(UISearchController(searchResultsController: nil), animated: true, completion: nil)
    }
}

And that's it. This is what I'm seeing: this

Presenting the search when the device is in portrait mode looks great in portrait - but if you rotate the device to landscape while presenting the searchbar, it will be wrongly positioned, a few pixels above the top of the screen.

Presenting the search when in landscape will yield the opposite. It looks great in landscape, but when rotating it to portrait the entire search controller view will be pushed down a few pixels.

It's not a matter of height size on the bar. The entire bar gets pushed up/down.

I tried investigating a bit further. I presented the search controller from landscape mode and rotated to portrait, and then debugged the view hierarchy:

debug

To be honest, I'm not quite sure what I'm looking at. The top-most view is a UISearchBarBackground embedded within a _UISearchBarContainerView, which is within a _UISearchControllerView.

As you can see in the size inspector on the right side, the middle-view "container" has y: 5, which makes no sense. When debugging the correct state, y is 0. What's really interesting is that the top-most view has y: -44 in this corrupt situation, and it has the same when it's correct - but you can clearly see that there is some space leftover above it. There seem to be three different y-positions. I don't get it.

I've read some guides on how to implement a UISearchController, but literally every single example I find is people modally presenting a custom ViewController that contains a SearchController. This will result in the entire custom ViewController being animated up from below.

Since the UISearchController is a subclass of UIViewController, I wanted to test out presenting it directly, not as part of a regular UIViewController. This gives the cool effect that the searchBar animates in from above, and the keyboard from below.

But why doesn't this work?

Edit: I just found out that if I enable Hide status bar in the project settings, the UISearchController looks even more correct in landscape than the "correct state" from above, and even animates correctly to portrait. It's super weird, because the status bar doesn't change at all. It was never visible in landscape. Why does this happen? It seems so buggy. Look at these three states: landscape

The first state is when showing search controller from portrait then rotating to landscape (doesn't matter if Hide status bar is enabled or not.

The second state is when showing search controller from landscape if Hide status bar is false

The third state is when showing search controller from landscape if Hide status bar is true.

4 Answers

As stated in the documentation:

Although a UISearchController object is a view controller, you should never present it directly from your interface. If you want to present the search results interface explicitly, wrap your search controller in a UISearchContainerViewController object and present that object instead.

Try to wrap your UISearchController inside a UISearchContainerViewController.

How about make a custom view contains search bar. Make it on top of the layers.

And when rotate occurs update frame/constraints of it.

This is covered in guidelines for designing for iPhone X. Now Navigation bar contains search controller. You can instanciate the search controller and set it to navigationItem.searchController. Now it will handle the search controller while rotation.

var search = UISearchController(searchResultsController: nil)
self.navigationItem.searchController = search

You can do this tric

class ViewController: UIViewController {

let sc = UISearchController(searchResultsController: nil)

override func viewDidLoad() {
    super.viewDidLoad()

    self.view.backgroundColor = .white
    let button = UIButton(frame: CGRect(x: 20, y: 200, width: 100, height: 40))
    button.setTitle("Search", for: .normal)
    button.backgroundColor = .black
    button.addTarget(self, action: #selector(click), for: .touchUpInside)
    self.view.addSubview(button)


}

@objc func click(){
    self.present(sc, animated: true, completion: nil)
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

    var mFrame = sc.view.frame
    mFrame.origin.y = 5.0
    sc.view.frame = mFrame
}
Related