Show search bar in navigation bar without scrolling on iOS 11 - ios

I’m attaching a UISearchController to the navigationItem.searchController property of a UITableViewController on iOS 11. This works fine: I can use the nice iOS 11-style search bar.
However, I’d like to make the search bar visible on launch. By default, the user has to scroll up in the table view to see the search bar. Does anyone know how is this is possible?
Left: default situation after launch. Right: search bar made visible (by scrolling up). I’d like to have the search bar visible after launch, as in the right screenshot.
I already found that the search bar can be made visible by setting the property hidesSearchBarWhenScrolling of my navigation item to false. However, this causes the search bar to always be visible — even when scrolling down —, which is not what I want.

The following makes the search bar visible at first, then allows it to hide when scrolling:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 11.0, *) {
navigationItem.hidesSearchBarWhenScrolling = false
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 11.0, *) {
navigationItem.hidesSearchBarWhenScrolling = true
}
}
Using isActive didn't do what I wanted, it makes the search bar active (showing cancel button, etc.), when all I want is for it to be visible.

You can set the property isActive to true after adding the searchController to the navigationItem.
Just like this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}

For me it worked after adding following lines in viewDidLoad() method:
navigationController?.navigationBar.prefersLargeTitles = true
navigationController!.navigationBar.sizeToFit()

On iOS 13, #Jordan Wood's answer didn't work.
Instead I did:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.performWithoutAnimation {
searchController.isActive = true
searchController.isActive = false
}
}

For (iOS 13.0, *) and SwiftUI
navigationController?.navigationBar.sizeToFit()
Example:
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationController?.navigationBar.sizeToFit()
}
.frame(width: 0, height: 0)
)
}
}

Related

Set navigation bar separator color for screen transition in Swift (iOS 13/14)?

I have 2 screens with a common navigation controller and common navigation bar. The first screen should have no separator, the second one should have a separator of a custom color.
Code I tried:
1)
navigationController?.navigationBar.shadowImage = ...//some image with an appropriate color
let appearance = UINavigationBarAppearance()
appearance.shadowImage = navBarSeparatorColor.as1ptImage()
navigationController?.navigationBar.scrollEdgeAppearance = appearance
It seems the first chunk of code works for ios 12 and that is all. Nothing works for iOS 13 or 14.
Note: there are some similar questions but their "solutions" don't work for iOS 13 and there are no questions for iOS 14 at all.
I think on iOS 13, you need to set the appearance object inside navigationBar, for example:
class FirstViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.standardAppearance.shadowColor = .clear
}
}
class SecondViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.standardAppearance.shadowColor = .red
}
}
Please note there are other appearances: compactAppearance and scrollEdgeAppearance. Instead of shadowColor, you can also set shadowImage as well.

Large title to small title switch in navigation bar is not smooth iOS 13, sticky

I have a UINavigationController with default value of it's navigationBar.prefersLargeTitles = true .
I am switching that to false when I push into a new scene lets call it (DetailsViewController), by changing it into the viewWillDisappear .
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.prefersLargeTitles = false
}
Now in DetailsViewController I am using willMove(to parent:) .
override func willMove(toParent parent: UIViewController?) {
navigationController?.navigationBar.prefersLargeTitles = true
}
To transition back to large titles .
Observe the attached snapshots of how iOS 13 doing it with how iOS 12 is doing it, considering iOS 12 is the correct behavior.
iOS 13 :
iOS 12 :
What you're doing was always wrong. You should set prefersLargeTitles to true once for the navigation bar and never touch it again.
The way to change what each view controller does about large titles as it appears is that that view controller sets its own navigationItem (in its viewDidLoad) to have the desired largeTitleDisplayMode. So if the first v.c. has .always and the second has .never everything will be smooth.
Swift 5, Xcode 13:
UIViewController(1) + UINavigationController:
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "Your title here"
}
UIViewController(2 - "i.e.: detailsViewController"):
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.largeTitleDisplayMode = .never
navigationItem.title = "Your title here"
}
It works like a charm!
I had the same issue and had to place a NavigationItem on the second ViewController's storyboard. My NavigationItem was being created automatically by the segue and its prefersLargeTitle in the viewDidLoad() was not finished creating before the view appeared. Adding a NavigationItem to the storyboard fixed this issue and allowed me to set the prefersLargeTitle in the storyboard's properties menu.
In my case this problem was occurring during a segue to a view controller which is a child of a UITabBarController. Setting largeTitleDisplayMode on the child view controller was not enough to fix this bug.
I have solved the issue by adding a navigation item to the UITabBarController scene and setting largeTitleDisplayMode as .never there.
I solved this problem like this:
override func viewWillDisappear(_ animated: Bool) {
title = ""
}
All ingenious is simple))
final class CustomHosting<Content: View>: UIHostingController<Content> {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.largeTitleDisplayMode = .never
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationItem.largeTitleDisplayMode = .automatic
}
}
I fixed same problem like this :) My problem is presenting SUI Detail Collection view from UIKit Collection, and had some jumping while navigation title changing.

How do I hide the status bar in an iOS app without causing the UINavigationBar to jump? [duplicate]

I have code that enters full screen mode by hiding the UINavigationController's navigation bar. I want a smooth animated zooming effect when entering full screen. I use setNavigationBarHidden(_:animated:). This has all worked fine up to now, even on iOS 11, but on iPhone X the animation is not working well. On hiding, there is no animation and the nav bar just disappears. On unhiding, it does animate but the nav bar appears at a slower rate than the navigation controller's content area reduces, so an ugly black background shows through the navigation bar area during the animation.
I can recreate this in a simple test app. I have a UIViewController embedded in a UINavigationController.
Storyboard
UINavigationController Navigation Bar: Style == Black; Translucent OFF
UIViewController: Extend Edges: all options OFF.
I have tried all the combinations of Adjust Scroll View Insets and Extend Edges that I can think of but they made no difference.
Code
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setFullScreen(on: fullScreen, animated: animated)
}
override var prefersStatusBarHidden: Bool
{
return fullScreen
}
override var preferredStatusBarStyle: UIStatusBarStyle
{
return .lightContent
}
#IBAction func onToggleNavBarVisibility(_ sender: Any) {
if let navBarHidden = self.navigationController?.isNavigationBarHidden {
// Toggle the state
fullScreen = !navBarHidden
setFullScreen(on: fullScreen, animated: true)
}
}
private func setFullScreen(on : Bool, animated : Bool) {
self.navigationController?.setNavigationBarHidden(on, animated: animated)
self.setNeedsStatusBarAppearanceUpdate()
}
In your case you are using both barTintColor & navigationBarStyle with Show Hide animation.
barTintColor overrides the value implied by the Style attribute
You should select either barTintColor or navigationBarStyle
In below code i have just used barTintColor & navigationBarStyle is default with Transulent.
var fullScreen = false{
didSet{
self.setNeedsStatusBarAppearanceUpdate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Navigation Bar"
navigationController?.navigationBar.barTintColor = .red
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
setFullScreen(on: fullScreen, animated: animated)
}
#IBAction func onToggleNavBarVisibility(_ sender: Any) {
if let navBarHidden =
self.navigationController?.isNavigationBarHidden {
// Toggle the state
fullScreen = !navBarHidden
setFullScreen(on: fullScreen, animated: true)
}
}
private func setFullScreen(on : Bool, animated : Bool) {
self.navigationController?.setNavigationBarHidden(on, animated: animated)
self.setNeedsStatusBarAppearanceUpdate()
}
EDIT:
If you want to hide status bar-
use prefersStatusBarHidden with the bool value. & use setNeedsStatusBarAppearanceUpdate
override var prefersStatusBarHidden: Bool {
return fullScreen
}
https://developer.apple.com/documentation/uikit/uinavigationbar
That's clearly a UIKit bug. I've filed FB8980917:
When hiding the navigation bar simultaneously with the status bar
using a slide animation, the navigation bar hides without animation.
In the opposite direction, the status bar appears with a fade
animation instead of the specified slide animation.
To reproduce, run the attached sample project. Use Simulator's slow
animations or record the device's screen and step through the frames.
I've also attached a "Screen video.mp4" for your reference.
Note 1: As a workaround, we could resort to the deprecated
UIApplication.setStatusBarHidden(_:with:) API (see "Screen video
legacy.mp4"). This mostly works except that the status bar animation
duration is longer than the navigation bar animation duration.
However, it requires setting
UIViewControllerBasedStatusBarAppearance=NO in Info.plist so it's an
all or nothing approach which opts out the whole app of the modern
API.
Note 2: Returning .fade for preferredStatusBarUpdateAnimation doesn't
work either. First, it's ugly because the navigation bar still slides
out (and can't be configured to fade out), second, the problem of the
missing hide animation of the navigation bar persists.
Note 3: Using UINavigationController's hidesBarsOnTap property doesn't
work either. The problem remains. The sample app also has
hidesBarsOnTap enabled.
Sample code:
class ViewController: UIViewController {
var fullScreen = false
override var prefersStatusBarHidden: Bool {
return navigationController!.isNavigationBarHidden
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
#IBAction func toggleFullscreen(_ sender: Any) {
fullScreen = !fullScreen
navigationController?.setNavigationBarHidden(fullScreen, animated: true)
setNeedsStatusBarAppearanceUpdate()
}
}
While the workaround described in Note 1 kind of works, I can't recommend it since the API is deprecated since iOS 9.0. So really, it's at the folks #Apple to fix this. The fact that apps such as Photos implement a similar behavior without that bug show that there is a way to do it, albeit with private API or ugly hacks.

setNavigationBarHidden animation not working as expected on iPhone X

I have code that enters full screen mode by hiding the UINavigationController's navigation bar. I want a smooth animated zooming effect when entering full screen. I use setNavigationBarHidden(_:animated:). This has all worked fine up to now, even on iOS 11, but on iPhone X the animation is not working well. On hiding, there is no animation and the nav bar just disappears. On unhiding, it does animate but the nav bar appears at a slower rate than the navigation controller's content area reduces, so an ugly black background shows through the navigation bar area during the animation.
I can recreate this in a simple test app. I have a UIViewController embedded in a UINavigationController.
Storyboard
UINavigationController Navigation Bar: Style == Black; Translucent OFF
UIViewController: Extend Edges: all options OFF.
I have tried all the combinations of Adjust Scroll View Insets and Extend Edges that I can think of but they made no difference.
Code
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setFullScreen(on: fullScreen, animated: animated)
}
override var prefersStatusBarHidden: Bool
{
return fullScreen
}
override var preferredStatusBarStyle: UIStatusBarStyle
{
return .lightContent
}
#IBAction func onToggleNavBarVisibility(_ sender: Any) {
if let navBarHidden = self.navigationController?.isNavigationBarHidden {
// Toggle the state
fullScreen = !navBarHidden
setFullScreen(on: fullScreen, animated: true)
}
}
private func setFullScreen(on : Bool, animated : Bool) {
self.navigationController?.setNavigationBarHidden(on, animated: animated)
self.setNeedsStatusBarAppearanceUpdate()
}
In your case you are using both barTintColor & navigationBarStyle with Show Hide animation.
barTintColor overrides the value implied by the Style attribute
You should select either barTintColor or navigationBarStyle
In below code i have just used barTintColor & navigationBarStyle is default with Transulent.
var fullScreen = false{
didSet{
self.setNeedsStatusBarAppearanceUpdate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Navigation Bar"
navigationController?.navigationBar.barTintColor = .red
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
setFullScreen(on: fullScreen, animated: animated)
}
#IBAction func onToggleNavBarVisibility(_ sender: Any) {
if let navBarHidden =
self.navigationController?.isNavigationBarHidden {
// Toggle the state
fullScreen = !navBarHidden
setFullScreen(on: fullScreen, animated: true)
}
}
private func setFullScreen(on : Bool, animated : Bool) {
self.navigationController?.setNavigationBarHidden(on, animated: animated)
self.setNeedsStatusBarAppearanceUpdate()
}
EDIT:
If you want to hide status bar-
use prefersStatusBarHidden with the bool value. & use setNeedsStatusBarAppearanceUpdate
override var prefersStatusBarHidden: Bool {
return fullScreen
}
https://developer.apple.com/documentation/uikit/uinavigationbar
That's clearly a UIKit bug. I've filed FB8980917:
When hiding the navigation bar simultaneously with the status bar
using a slide animation, the navigation bar hides without animation.
In the opposite direction, the status bar appears with a fade
animation instead of the specified slide animation.
To reproduce, run the attached sample project. Use Simulator's slow
animations or record the device's screen and step through the frames.
I've also attached a "Screen video.mp4" for your reference.
Note 1: As a workaround, we could resort to the deprecated
UIApplication.setStatusBarHidden(_:with:) API (see "Screen video
legacy.mp4"). This mostly works except that the status bar animation
duration is longer than the navigation bar animation duration.
However, it requires setting
UIViewControllerBasedStatusBarAppearance=NO in Info.plist so it's an
all or nothing approach which opts out the whole app of the modern
API.
Note 2: Returning .fade for preferredStatusBarUpdateAnimation doesn't
work either. First, it's ugly because the navigation bar still slides
out (and can't be configured to fade out), second, the problem of the
missing hide animation of the navigation bar persists.
Note 3: Using UINavigationController's hidesBarsOnTap property doesn't
work either. The problem remains. The sample app also has
hidesBarsOnTap enabled.
Sample code:
class ViewController: UIViewController {
var fullScreen = false
override var prefersStatusBarHidden: Bool {
return navigationController!.isNavigationBarHidden
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
#IBAction func toggleFullscreen(_ sender: Any) {
fullScreen = !fullScreen
navigationController?.setNavigationBarHidden(fullScreen, animated: true)
setNeedsStatusBarAppearanceUpdate()
}
}
While the workaround described in Note 1 kind of works, I can't recommend it since the API is deprecated since iOS 9.0. So really, it's at the folks #Apple to fix this. The fact that apps such as Photos implement a similar behavior without that bug show that there is a way to do it, albeit with private API or ugly hacks.

Back button animation with large title jumps

We have two UIViewController with an UINavigationController.
In the first presented VC inside of viewWillAppear(_ animated: Bool) we do:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = true
navigationController?.navigationItem.largeTitleDisplayMode = .always
}
....
Inside of the second VC we deactive that behaviour with inside of viewWillAppear(_ animated: Bool):
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = false
}
...
The transition animation to the second VC is smooth while tapping automatic generated back button causes the navigation controller title to create a strange jump to large title instead of the normal grow to large title animation as it does for example in the Messages App.
If i tap the tabbar icon as "back" operation, it does the right transition animation.
Any idea what could cause that issue or how i can fix it?
on the second view controller set the largeTitleDisplayMode to .never
you won't need to set the prefersLargeTitles to false.
To clarify things here, you've to set the largeTitleDisplayMode directly for the navigationItem of the view controller, not the navigation controller!
self.navigationItem.largeTitleDisplayMode = .never // This fixes the issue
self.navigationController?.navigationItem.largeTitleDisplayMode = .never // This doesn't work / Title will stay large
#dave's answer worked for me! Thanks! Here's the code that I used in its entirety:
FirstViewController:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 11.0, *) {
navigationItem.largeTitleDisplayMode = .always
navigationController?.navigationBar.prefersLargeTitles = true
}
}
}
SecondViewController:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 11.0, *) {
navigationItem.largeTitleDisplayMode = .never
}
}
}
One should make force layout of navigation bar right after switching off large title
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.prefersLargeTitles = false
navigationController?.navigationBar.layoutIfNeeded()
}
This cancels out large navigation title immediately.
I had the same transition bug: from large title to small one or backwards. It was not growing/diminishing from one state to the another, but it was staying ugly on the screen for 1 sec, then just jumping from large to small or vice-versa.
The simple solution:
Make sure each view controller has a navigationItem in the Storyboard.
And for each navigationItem, set the corresponding Large Title
value:
Also, you don't need to set anything in viewDidLoad,viewWillAppear, etc. related to largeTitle. Just what I showed above.
For me it was something completely different. In my project we set a custom back button without title on every VC. Standard way to do this for ages was to set an empty BarButtonItem like this:
navigationItem.backBarButtonItem = UIBarButtonItem()
Removing that line fixed the jumping back button when moving from a VC with large title to one without large title. Still having design requirement I found out that since iOS 14 this can be done much more neatly:
navigationItem.backButtonDisplayMode = .minimal
So just replace setting a new BarButtonItem with setting the display mode.

Resources