After adapting to the new iOS 11 UISearchController in the NavigationItem, I encountered some problems.
I've added a UIBarButtonItem to the toolbar of the search bar of a UISearchController. This button calls a function when clicked (passed as the action-parameter in the UIBarButtonItem constructor).
Prior to iOS 11, the search bar has been attached to the tableHeaderView, and this worked (and still works) perfectly. The function is called when the button is clicked.
However, in iOS 11 the function is not called even though the implementation is the same.
Any ideas what could be wrong? Or is a bug in iOS 11?
private func setupSearchController() {
...
let toolbar = UIToolbar()
toolbar.sizeToFit()
// Create bar button item with image.
let qrBarButton = UIBarButtonItem(image: #imageLiteral(resourceName: "QR Code"), style: .plain, target: nil, action: #selector(didPressQRToolbarButton))
// Add the new button.
toolbar.items = [qrBarButton]
searchController.searchBar.inputAccessoryView = toolbar
// If the device is on iOS 11, use the "native" search bar placement.
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
// Don't use the large title in the navigation bar.
navigationController?.navigationBar.prefersLargeTitles = false
} else {
// Handled in subclasses.
}
}
/// Action for the QR toolbar button
func didPressQRToolbarButton(sender: Any) {
...
// NOT CALLED
}
First, check the sender of the function func didPressQRToolbarButton(sender: Any).
It should be like this:
func didPressQRToolbarButton(sender: UIBarButtonItem)
Then, you must be getting some warning from xCode since the automatic #objc inference is deprecated on Swift 4.
So, on Swift 4, didPressQRTToolbar or any function passed as an argument to #selector:
#selector(didPressQRToolbarButton)
must add #objc on its declaration:
#objc func didPressQRToolbarButton(sender: Any) {
The problem was fixed by passing self as the target in the UIBarButtonItem contructor. Somehow this wasn't needed prior to iOS 11.
This fix along with festeban26's answer fixed the problem.
Related
Update: This bug has been fixed in iOS 14.5
I have the following class embedded in a UINavigationController:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let barButton = UIBarButtonItem(title: "Save", style: .plain, target: nil, action: nil)
barButton.accessibilityLabel = "Save meeting"
navigationItem.rightBarButtonItem = barButton
}
}
When running iOS 14.4, the accessibility label is ignored and only the visible title is announced by VoiceOver. However, on iOS 13.7, the accessibility label is correctly used. Has the UIBarButtonItem usage changed or is this an iOS bug?
Screenshot for context:
When I must implement a UIBarButtonItem, I always follow these instructions to be sure that a11y will be stable and completely functional. 👍
I don't know if this situation is a bug or a kind of regression due to the new iOS version but implementing a11y in the navigation bar buttons as customization is a perfect way to encounter no unfortunate surprises even if it looks like a boilerplate solution. 🤓
I've created a blank project with a simple view controller embedded in a navigation controller where a right bar button is displayed as follows:
class NavBarViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var a11yRightBarButton: UIBarButtonItem?
let a11y = UILabel()
a11y.text = "OK"
a11y.sizeToFit()
a11y.isUserInteractionEnabled = true //Mandatory to use the 'tap gesture'.
a11yRightBarButton = UIBarButtonItem(customView: a11y)
let tap = UITapGestureRecognizer(target: self,
action: #selector(validateActions(info:)))
a11yRightBarButton?.customView?.addGestureRecognizer(tap)
a11yRightBarButton?.isAccessibilityElement = true
a11yRightBarButton?.accessibilityTraits = .button
a11yRightBarButton?.accessibilityLabel = "validate your actions"
navigationItem.rightBarButtonItem = a11yRightBarButton
}
#objc func validateActions(info: Bool) -> Bool {
print("hello")
return true
}
}
Your right bar button displays "OK" and VoiceOver reads out "validate your actions" under iOS 14.4 and Xcode 12.4. 😉
Following this rationale, you can use a UIBarButtonItem as supporting the accessibilityLabel property in iOS 14. 🎉
Set accessibility of UIBarButtonItem to true.
let barButton = UIBarButtonItem(title: "Save", style: .plain, target: nil, action: nil)
barButton.accessibilityLabel = "Save meeting"
barButton.isAccessibilityElement = true
navigationItem.rightBarButtonItem = barButton
I got a navigation bar containing some UIBarButtonItem buttons and a UISearchBar hooked up like this
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
title = "Test"
tableView.delegate = self
tableView.dataSource = self
searchController = UISearchController(searchResultsController: nil)
navigationItem.searchController = searchController
// This leads to the bug
searchController.hidesNavigationBarDuringPresentation = false
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(leftTapped))
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(rightTapped))
}
Scenario: I tap into the search bar and tap cancel afterwards.
Issue 1: The bar buttons are not reacting to touch except when I touch the outer most pixels of the screen (only possible with the simulator and mouse clicks).
Issue 2: The navigation items are overlapping when I push another view controller.
When I use hidesNavigationBarDuringPresentation = true it's working like expected.
The issue appears on notched and non-notched iPhones iOS 13.0 and 13.1 using Xcode 11.0 and 11.1.
Here's the whole test project:
https://github.com/fl034/HidesNavigationBarDuringPresentationTest
I've filed a radar (and you should too), but maybe some of you guys have already a workaround for it?
Update 1: Bug is still there in iOS 13.1.1
Update 2: Bug is fixed in iOS 13.2 beta (thanks #Ben Gomm)
The view debugger reveals what's going on with this bug. The contents of the navigation bar are being copied. Here's what the navigation bar looks like before you show the search:
And here's what it looks like afterwards:
The two replicant views and the extra UILabel are the problem. I don't know what they're doing there and I can't find a way to remove them.
EDIT By the way, I think some of Apple's apps display the same bug. It's easier to see if you have large titles, because then you can see the large title and the extra label at the same time:
I'm now using this workaround as I want most of my users have the navigation bar visible while search is active (for several app-ux-specific reasons).
var isIosVersionWithNavigationBarBug: Bool {
if #available(iOS 13.2, *) {
return false
}
if #available(iOS 13.0, *) {
return true
}
return false
}
In my search controller I use it like this
mySearchController.hidesNavigationBarDuringPresentation = isIosVersionWithNavigationBarBug
So if iOS 13.2 is being released and the user updates to it, the workaround is not being applied anymore.
This appears to be fixed in iOS 13.2 beta, I tested the example project above using Xcode 11.2 beta (11B41).
Not proud of it but I got it working for now with this hack.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let viewsToRemove = self.navigationController?.navigationBar.subviews.flatMap({ (view) in
view.subviews.filter { type(of: $0) == UILabel.self }
})
viewsToRemove?.forEach { $0.removeFromSuperview() }
}
Mine screen contain UITabBar (tabs) and childViewController (content).
When I switch to another tab, I replace current child with next code:
private func replaceChild(_ old: UIViewController?, with new: UIViewController, in container: UIView) {
old?.view.removeFromSuperview()
old?.removeFromParentViewController()
container.addSubviewWithConstraints(new.view)
self.addChildViewController(new)
self.replaceNavBar(with: new)
}
Also I try to steal barButtons from child and put them in VC:
private func replaceNavBar(with new: UIViewController) {
self.title = new.title
self.navigationItem.leftBarButtonItems = new.navigationItem.leftBarButtonItems
self.navigationItem.rightBarButtonItems = new.navigationItem.rightBarButtonItems
}
Console check rightBarButtonItem setting for first tab (right button +):
Before:
oldVC: nil
newVC: <UIBarButtonItem: 0x7fb5f742bb50> target=0x7fb5f907c800 action=createNewMessage systemItem=Add
After:
oldVC: <UIBarButtonItem: 0x7fb5f742bb50> target=0x7fb5f907c800 action=createNewMessage systemItem=Add
Also if I set nil (for tabs, where no barButtons needed) - in debugger all show's OK and no error message happens.
But on UI, all buttons stays the same (if first tab was with buttons - buttons displayed and actions triggered. If it was no buttons -> no buttons). I store childs in array in main VC - so, no reinit happens
I'm pretty sure, that it's possible, because I've implement this flow some time ago and it works correct (put's child buttons to main VC, but stores target and selector), but I lost that code
Question: How I can steal barButtonItems from child VC and put them in parent VC navItem?
Upd
I checked barButton setting with dumb code and it worked, but not on UI: breakpoints inside if and else blocks called by turns, but no button was added to navItem:
if let _ = self.navigationItem.rightBarButtonItem {
self.navigationItem.rightBarButtonItem = nil
} else {
let addButton = UIBarButtonItem.init(barButtonSystemItem: .add, target: nil, action: nil)
addButton.tintColor = .white
self.navigationItem.rightBarButtonItem = addButton
}
Also I tried version with self.navigationController?.navigationItem.rightBarButtonItem
Swift 4.1, Xcode 9.4.1
Solution found:
I forgot about very important thing - my VC is child for another one VC.
So I should perform not the self.navigationItem.rightBarButtonItem = ... but self.parent?.navigationItem.rightBarButtonItem = ...
The first screenshot is taken before playing video in full screen.
The second is taken after the video is opened in full screen and closed.
Any idea why navigation toolbar has extend?
Note: The hamburger button is not the part of the navigation item. It is faked in overlay in parent that holds its child controller inside standard container.
Nothing special inside the source:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
bbiListic = UIBarButtonItem(image: UIImage(identifier: .IcoHeaderListic), style: .Plain, target: self, action: #selector(UIViewController.showListic))
bbiFavorite = UIBarButtonItem(image: UIImage(identifier: .IcoHeaderStarEmpty), style: .Plain, target: self, action: #selector(LiveDogadjajViewController.toggleFavorite(_:)))
...
let items = [bbiListic!,bbiFavorite!]
navigationItem.rightBarButtonItems = items
}
func someRefresh() {
var items = [UIBarButtonItem]()
items.append(bbiListic!)
...
navigationItem.rightBarButtonItems = items
}
Update:
This appears to be a problem only on the latest version of iOS, 9.3
From your screenshots, it look like that height of status bar gets doubled. Try this:-
Before playing your video, hide the status bar
UIApplication.sharedApplication().setStatusBarHidden(true, withAnimation: .None)
After ending the video, show the status bar
UIApplication.sharedApplication().setStatusBarHidden(false, withAnimation: .None)
Pre-requisite:
a) Uncheck "extends edges" by selecting your uiviewcontroller from main.storyboard
b) no constraints exist between your video player and container controller
Solution:
Check if any of your buttons on the nav bar are bottom constrained. Either remove that constraint or apply a fixed height constraint to your custom nav bar view so that it stays the same height.
I contacted the Apple developer technical support about this issue.
They have determined it is probably a bug in iOS 9.3.
Bug id: 26439832, iOS SDK
This is easy workaround for view controller on stack:
// ... add this to init method
let nc = NSNotificationCenter.defaultCenter()
nc.addObserver(self, selector: #selector(didExitFullscreen(_:)), name: MPMoviePlayerDidExitFullscreenNotification, object: nil)
func didExitFullscreen(notification: NSNotification)
{
// hack, fix for 9.3.
if #available(iOS 9.3, *)
{
navigationController?.setNavigationBarHidden(true, animated: false)
navigationController?.setNavigationBarHidden(false, animated: false)
}
}
I have UICollectionView. On clicking search button in UINavigationBar, I am adding the UISearchController's searchbar as titleview for UINavigationItem. For iPhone it is working properly. For iPad the cancel button is not shown. The Searchbar alone takes the entire width.
Can anyone help me out on this?. Thanks in advance.
iOS7 does not show the cancel button when added to a navigation bar.You can put searchbar in another view like this.
UISearchBar *searchBar = [UISearchBar new];
searchBar.showsCancelButton = YES;
[searchBar sizeToFit];
UIView *viewForSearchBar = [[UIView alloc]initWithFrame:searchBar.bounds];
[viewForSearchBar addSubview:searchBar];
self.navigationItem.titleView = viewForSearchBar;
I had the same problem, on iPhone the search cancel was shown well, but on iPad it didn't.
The workaround of wrapping the UISearchBar in another UIView didn't work well for me since it had different appearance and wrong width on rotation.
My solution is a simple one - use search WITHOUT cancel, and add cancel as a UIBarButtonItem.
Added rightBarButtonItem with selector will work fine for me. And adding searchBar inside view before setting to navigation title view was not showing properly.
Code:-
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Cancel", style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.dismissView))
func dismissView() {
if self.controller?.navigationController?.popViewController(animated: true) == nil {
self.controller?.dismiss(animated: true, completion: nil)
}
}
As per apple documentation setShowsCancelButton
Cancel buttons are not displayed for apps running on iPad, even when
you specify YES for the showsCancelButton parameter.
I am not sure about the alternate but this is what apple provides us.
Try this. Add a checkmark for shows cancel button.
Swift version :-
I tried the #Nikita Khandelwal method, but still it doesn't fit for ipad view. Here is the swift code, which was given as corrected answer :-
let searchBar: UISearchBar = UISearchBar()
searchBar.showCancelButton = true
searchBar.placeholder = "Search Your Job Title"
searchBar.fitToSize()
searchBar.delegate = self //do not need if you delegate searchBar
let viewForSearchBar: UIView = UIView(frame: searchBar.bounds)
viewForSearchBar.addSubview(searchBar)
self.navigationItem.titleView = viewForSearchBar
********* But There is another way to set cancel button correctly and fit for the view :-
Set search bar as the Navigation bar title view :-
let searchBar: UISearchBar = UISearchBar()
searchBar.showCancelButton = true
searchBar.placeholder = "Search Your Job Title"
searchBar.delegate = self //do not need if you delegate searchBar
self.navigationItem.titleView = searchBar
Drag and drop Bar button to the right side of the view controller & name it as Cancel.
Then connect that button to this function :-
#IBAction func iPadCancelButton(sender: AnyObject) {
UIApplication.sharedApplication().sendAction("resignFirstResponder", to:nil, from:nil, forEvent:nil)
self.dismissViewControllerAnimated(true, completion: nil)
}
For iOS 13 built with Xcode 11, I'm needing to set manually set the display value on the cancel button, depending on whether the search controller is visible