UIToolBar position to top of UINavigationController - ios

How to move the UIToolBar to top (stick to the UINavigationBar)?
I m struggle with this thing for a long time and I've try some stuff like:
Custom UIToolBar that conforms to UIToolbarDelegate and (UIBarPosition)positionForBar:(id <UIBarPositioning>)bar get called
and I return UIBarPositionTop but the toolbar stays at bottom.
Change the toolbar frame: self.navigationController.toolbar.frame = CGRectMake(0, NAV_BAR_Y, self.view.bounds.size.width, NAV_BAR_HEIGHT);
Custom UINaviagtionController which has this delegate function: (UIBarPosition)positionForBar:(id <UIBarPositioning>)bar {
return UIBarPositionTop;
}
None of the struggles goes well, same look:
Any Help will be great.
(I would like to have navigation look as Apple App store navigation)

There are 2 options that I'm aware of.
1) Related to Move UINavigationController's toolbar to the top to lie underneath navigation bar
You can subclass UINavigationController and change the Y-axis position of the toolbar when the value is set.
import UIKit
private var context = 0
class NavigationController: UINavigationController {
private var inToolbarFrameChange = false
var observerBag: [NSKeyValueObservation] = []
override func awakeFromNib() {
super.awakeFromNib()
self.inToolbarFrameChange = false
}
override func viewDidLoad() {
super.viewDidLoad()
observerBag.append(
toolbar.observe(\.center, options: .new) { toolbar, _ in
if !self.inToolbarFrameChange {
self.inToolbarFrameChange = true
toolbar.frame = CGRect(
x: 0,
y: self.navigationBar.frame.height + UIApplication.shared.statusBarFrame.height,
width: toolbar.frame.width,
height: toolbar.frame.height
)
self.inToolbarFrameChange = false
}
}
)
}
override func setToolbarHidden(_ hidden: Bool, animated: Bool) {
super.setToolbarHidden(hidden, animated: false)
var rectTB = self.toolbar.frame
rectTB = .zero
}
}
2) You can create your own UIToolbar and add it to view of the UIViewController. Then, you add the constraints to the leading, trailing and the top of the safe area.
import UIKit
final class ViewController: UIViewController {
private let toolbar = UIToolbar()
private let segmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: ["Op 1", "Op 2"])
control.isEnabled = false
return control
}()
override func loadView() {
super.loadView()
setupToolbar()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.hideBorderLine()
}
private func setupToolbar() {
let barItem = UIBarButtonItem(customView: segmentedControl)
toolbar.setItems([barItem], animated: false)
toolbar.isTranslucent = false
toolbar.isOpaque = false
view.addSubview(toolbar)
toolbar.translatesAutoresizingMaskIntoConstraints = false
toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
}
}
private extension UINavigationBar {
func showBorderLine() {
findBorderLine().isHidden = false
}
func hideBorderLine() {
findBorderLine().isHidden = true
}
private func findBorderLine() -> UIImageView! {
return self.subviews
.flatMap { $0.subviews }
.compactMap { $0 as? UIImageView }
.filter { $0.bounds.size.width == self.bounds.size.width }
.filter { $0.bounds.size.height <= 2 }
.first
}
}

Try this solution
#interface ViewController () <UIToolbarDelegate>
{
UIToolbar * lpToolbar;
}
#end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
lpToolbar = [[UIToolbar alloc] initWithFrame :CGRectZero];
lpToolbar.delegate = self;
self.navigationItem.title = #"Title";
}
-(void) viewWillAppear :(BOOL)animated
{
[super viewWillAppear:animated];
[self.navigationController.view addSubview :lpToolbar];
CGRect rFrame = self.navigationController.navigationBar.frame;
lpToolbar.frame = CGRectMake( 0.0, rFrame.origin.y + rFrame.size.height, rFrame.size.width, 50.0 );
}
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[lpToolbar removeFromSuperview];
}
-(UIBarPosition) positionForBar:(id <UIBarPositioning>)bar
{
return UIBarPositionTop;
}

Related

How to preserve space occupied by status bar when hiding status bar animately?

I tend to hide the status bar, animated in the following way.
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
extension ViewController: SideMenuNavigationControllerDelegate {
func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = true
}
func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = false
}
}
However, I would also like to preserve the space occupied by status bar, so that when status bar appears, the entire app will not be "pushed up"
May I know how I can achieve so?
Thank you.
You can use additionalSafeAreaInsets to add a placeholder height, substituting the status bar.
But for devices with a notch like the iPhone 12, the space is automatically preserved, so you don't need to add any additional height.
class ViewController: UIViewController {
var statusBarHidden: Bool = false /// no more computed property, otherwise reading safe area would be too late
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
#IBAction func showButtonPressed(_ sender: Any) {
statusBarHidden.toggle()
if statusBarHidden {
sideMenuWillAppear()
} else {
sideMenuWillDisappear()
}
}
lazy var overlayViewController: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "OverlayViewController")
}()
var additionalHeight: CGFloat {
if view.window?.safeAreaInsets.top ?? 0 > 20 { /// is iPhone X or other device with notch
return 0 /// add 0 height
} else {
/// the height of the status bar
return view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0
}
}
}
extension ViewController {
/// add placeholder height to substitute status bar
func addAdditionalHeight(_ add: Bool) {
if add {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = additionalHeight
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = additionalHeight
}
} else {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = 0
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = 0
}
}
}
func sideMenuWillAppear() {
addChild(overlayViewController)
view.addSubview(overlayViewController.view)
overlayViewController.view.frame = view.bounds
overlayViewController.view.frame.origin.x = -400
overlayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
overlayViewController.didMove(toParent: self)
addAdditionalHeight(true) /// add placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -100
self.setNeedsStatusBarAppearanceUpdate() /// hide status bar
}
}
func sideMenuDidAppear() {}
func sideMenuWillDisappear() {
addAdditionalHeight(false) /// remove placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -400
self.setNeedsStatusBarAppearanceUpdate() /// show status bar
} completion: { _ in
self.overlayViewController.willMove(toParent: nil)
self.overlayViewController.view.removeFromSuperview()
self.overlayViewController.removeFromParent()
}
}
func sideMenuDidDisappear() {}
}
Result (Tested on iPhone 12, iPhone 8, iPad Pro 4th gen):
iPhone 12 (notch)
iPhone 8 (no notch)
iPhone 12 + navigation bar
iPhone 8 + navigation bar
Demo GitHub repo
First of all, it is not currently possible to make UINavigationController behave this way. However you can wrap your UINavigationController instance in a Container View Controller.
This will give you control over managing the top space from where the UINavigationController view layout starts. Inside this container class, you could manage it like following -
class ContainerViewController: UIViewController {
private lazy var statusBarBackgroundView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var statusBarBackgroundViewHeightConstraint: NSLayoutConstraint = {
statusBarBackgroundView.heightAnchor.constraint(equalToConstant: 0)
}()
var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
guard let statusBarMananger = self.view.window?.windowScene?.statusBarManager
else { return 0 }
return statusBarMananger.statusBarFrame.height
} else {
return UIApplication.shared.statusBarFrame.height
}
}
var statusBarHidden: Bool = false {
didSet {
self.statusBarBackgroundViewHeightConstraint.constant = self.statusBarHidden ? self.lastKnownStatusBarHeight : 0
self.view.layoutIfNeeded()
}
}
private var lastKnownStatusBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let topView = self.statusBarBackgroundView
self.view.addSubview(topView)
NSLayoutConstraint.activate([
topView.topAnchor.constraint(equalTo: self.view.topAnchor),
topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
statusBarBackgroundViewHeightConstraint,
topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = self.statusBarHeight
if height > 0 {
self.lastKnownStatusBarHeight = height
}
}
func setUpNavigationController(_ navCtrl: UINavigationController) {
self.addChild(navCtrl)
navCtrl.didMove(toParent: self)
self.view.addSubview(navCtrl.view)
navCtrl.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navCtrl.view.topAnchor.constraint(equalTo: statusBarBackgroundView.bottomAnchor),
navCtrl.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
navCtrl.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
navCtrl.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.view.layoutIfNeeded()
}
}
Now from your call site, you can do following -
class ViewController: UIViewController {
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
/// Forward the call to ContainerViewController to act on this update
(self.navigationController?.parent as? ContainerViewController)?.statusBarHidden = self.statusBarHidden
/// Keep doing whatever you are doing now
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
}

Hiding/showing status bar makes navigation bar jump down [duplicate]

I tend to hide the status bar, animated in the following way.
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
extension ViewController: SideMenuNavigationControllerDelegate {
func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = true
}
func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = false
}
}
However, I would also like to preserve the space occupied by status bar, so that when status bar appears, the entire app will not be "pushed up"
May I know how I can achieve so?
Thank you.
You can use additionalSafeAreaInsets to add a placeholder height, substituting the status bar.
But for devices with a notch like the iPhone 12, the space is automatically preserved, so you don't need to add any additional height.
class ViewController: UIViewController {
var statusBarHidden: Bool = false /// no more computed property, otherwise reading safe area would be too late
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
#IBAction func showButtonPressed(_ sender: Any) {
statusBarHidden.toggle()
if statusBarHidden {
sideMenuWillAppear()
} else {
sideMenuWillDisappear()
}
}
lazy var overlayViewController: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "OverlayViewController")
}()
var additionalHeight: CGFloat {
if view.window?.safeAreaInsets.top ?? 0 > 20 { /// is iPhone X or other device with notch
return 0 /// add 0 height
} else {
/// the height of the status bar
return view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0
}
}
}
extension ViewController {
/// add placeholder height to substitute status bar
func addAdditionalHeight(_ add: Bool) {
if add {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = additionalHeight
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = additionalHeight
}
} else {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = 0
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = 0
}
}
}
func sideMenuWillAppear() {
addChild(overlayViewController)
view.addSubview(overlayViewController.view)
overlayViewController.view.frame = view.bounds
overlayViewController.view.frame.origin.x = -400
overlayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
overlayViewController.didMove(toParent: self)
addAdditionalHeight(true) /// add placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -100
self.setNeedsStatusBarAppearanceUpdate() /// hide status bar
}
}
func sideMenuDidAppear() {}
func sideMenuWillDisappear() {
addAdditionalHeight(false) /// remove placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -400
self.setNeedsStatusBarAppearanceUpdate() /// show status bar
} completion: { _ in
self.overlayViewController.willMove(toParent: nil)
self.overlayViewController.view.removeFromSuperview()
self.overlayViewController.removeFromParent()
}
}
func sideMenuDidDisappear() {}
}
Result (Tested on iPhone 12, iPhone 8, iPad Pro 4th gen):
iPhone 12 (notch)
iPhone 8 (no notch)
iPhone 12 + navigation bar
iPhone 8 + navigation bar
Demo GitHub repo
First of all, it is not currently possible to make UINavigationController behave this way. However you can wrap your UINavigationController instance in a Container View Controller.
This will give you control over managing the top space from where the UINavigationController view layout starts. Inside this container class, you could manage it like following -
class ContainerViewController: UIViewController {
private lazy var statusBarBackgroundView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var statusBarBackgroundViewHeightConstraint: NSLayoutConstraint = {
statusBarBackgroundView.heightAnchor.constraint(equalToConstant: 0)
}()
var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
guard let statusBarMananger = self.view.window?.windowScene?.statusBarManager
else { return 0 }
return statusBarMananger.statusBarFrame.height
} else {
return UIApplication.shared.statusBarFrame.height
}
}
var statusBarHidden: Bool = false {
didSet {
self.statusBarBackgroundViewHeightConstraint.constant = self.statusBarHidden ? self.lastKnownStatusBarHeight : 0
self.view.layoutIfNeeded()
}
}
private var lastKnownStatusBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let topView = self.statusBarBackgroundView
self.view.addSubview(topView)
NSLayoutConstraint.activate([
topView.topAnchor.constraint(equalTo: self.view.topAnchor),
topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
statusBarBackgroundViewHeightConstraint,
topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = self.statusBarHeight
if height > 0 {
self.lastKnownStatusBarHeight = height
}
}
func setUpNavigationController(_ navCtrl: UINavigationController) {
self.addChild(navCtrl)
navCtrl.didMove(toParent: self)
self.view.addSubview(navCtrl.view)
navCtrl.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navCtrl.view.topAnchor.constraint(equalTo: statusBarBackgroundView.bottomAnchor),
navCtrl.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
navCtrl.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
navCtrl.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.view.layoutIfNeeded()
}
}
Now from your call site, you can do following -
class ViewController: UIViewController {
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
/// Forward the call to ContainerViewController to act on this update
(self.navigationController?.parent as? ContainerViewController)?.statusBarHidden = self.statusBarHidden
/// Keep doing whatever you are doing now
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
}

Best way to position UIToolbar programmatically (with or without UIToolbarDelegate)?

I'm implementing in Playgound a segmented control underneath the navigation bar.
This seems to be a classic problem, which has been asked:
UISegmentedControl below UINavigationbar in iOS 7
Add segmented control to navigation bar and keep title with buttons
In the doc of UIBarPositioningDelegate, it says,
The UINavigationBarDelegate, UISearchBarDelegate, and
UIToolbarDelegate protocols extend this protocol to allow for the
positioning of those bars on the screen.
And In the doc of UIBarPosition:
case top
Specifies that the bar is at the top of its containing view.
In the doc of UIToolbar.delegate:
You may not set the delegate when the toolbar is managed by a
navigation controller. The default value is nil.
My current solution is as below (the commented-out code are kept for reference and convenience):
import UIKit
import PlaygroundSupport
class ViewController : UIViewController, UIToolbarDelegate
{
let toolbar : UIToolbar = {
let ret = UIToolbar()
let segmented = UISegmentedControl(items: ["Good", "Bad"])
let barItem = UIBarButtonItem(customView: segmented)
ret.setItems([barItem], animated: false)
return ret
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(toolbar)
// toolbar.delegate = self
}
override func viewDidLayoutSubviews() {
toolbar.frame = CGRect(
x: 0,
y: navigationController?.navigationBar.frame.height ?? 0,
width: navigationController?.navigationBar.frame.width ?? 0,
height: 44
)
}
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}
//class Toolbar : UIToolbar {
// override var barPosition: UIBarPosition {
// return .topAttached
// }
//}
let vc = ViewController()
vc.title = "Try"
vc.view.backgroundColor = .red
// Another way to add toolbar...
// let segmented = UISegmentedControl(items: ["Good", "Bad"])
// let barItem = UIBarButtonItem(customView: segmented)
// vc.toolbarItems = [barItem]
// Navigation Controller
let navVC = UINavigationController(navigationBarClass: UINavigationBar.self, toolbarClass: UIToolbar.self)
navVC.pushViewController(vc, animated: true)
navVC.preferredContentSize = CGSize(width: 375, height: 640)
// navVC.isToolbarHidden = false
// Page setup
PlaygroundPage.current.liveView = navVC
PlaygroundPage.current.needsIndefiniteExecution = true
As you can see, this doesn't use a UIToolbarDelegate.
How does a UIToolbarDelegate (providing the position(for:)) come into play in this situation? Since we can always position ourselves (either manually or using Auto Layout), what's the use case of a UIToolbarDelegate?
#Leo Natan's answer in the first question link above mentioned the UIToolbarDelegate, but it seems the toolbar is placed in Interface Builder.
Moreover, if we don't use UIToolbarDelegate here, why don't we just use a plain UIView instead of a UIToolbar?
Try this
UIView *containerVw = [[UIView alloc] initWithFrame:CGRectMake(0, 64, 320, 60)];
containerVw.backgroundColor = UIColorFromRGB(0xffffff);
[self.view addSubview:containerVw];
UIView *bottomView = [[UIView alloc] initWithFrame:CGRectMake(0, 124, 320, 1)];
bottomView.backgroundColor = [UIColor grayColor];
[self.view addSubview:bottomView];
UISegmentedControl *sg = [[UISegmentedControl alloc] initWithItems:#[#"Good", #"Bad"]];
sg.frame = CGRectMake(10, 10, 300, 40);
[view addSubview:sg];
for (UIView *view in self.navigationController.navigationBar.subviews) {
for (UIView *subView in view.subviews) {
[subView isKindOfClass:[UIImageView class]];
subView.hidden = YES;
}
}
By setting the toolbar's delegate and by having the delegate method return .top, you get the normal shadow at the bottom of the toolbar. If you also adjust the toolbars frame one point higher, it will cover the navbar's shadow and the final result will be what appears to be a taller navbar with a segmented control added.
class ViewController : UIViewController, UIToolbarDelegate
{
lazy var toolbar: UIToolbar = {
let ret = UIToolbar()
ret.delegate = self
let segmented = UISegmentedControl(items: ["Good", "Bad"])
let barItem = UIBarButtonItem(customView: segmented)
ret.setItems([barItem], animated: false)
return ret
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(toolbar)
toolbar.delegate = self
}
override func viewDidLayoutSubviews() {
toolbar.frame = CGRect(
x: 0,
y: navigationController?.navigationBar.frame.height - 1 ?? 0,
width: navigationController?.navigationBar.frame.width ?? 0,
height: toolbar.frame.height
)
}
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .top
}
}
How does a UIToolbarDelegate (providing the position(for:)) come into play in this situation? Since we can always position ourselves (either manually or using Auto Layout), what's the use case of a UIToolbarDelegate?
I sincerely do not know how the UIToolbarDelegate comes into play, if you change the UINavigationController.toolbar it will crashes with "You cannot set UIToolbar delegate managed by the UINavigationController manually", moreover the same will happen if you try to change the toolbar's constraint or its translatesAutoresizingMaskIntoConstraints property.
Moreover, if we don't use UIToolbarDelegate here, why don't we just use a plain UIView instead of a UIToolbar?
It seems to be a reasonable question. I guess the answer for this is that you have a UIView subclass which already has the behaviour of UIToolbar, so why would we create another class-like UIToolbar, unless you just want some view below the navigation bar.
There are 2 options that I'm aware of.
1) Related to Move UINavigationController's toolbar to the top to lie underneath navigation bar
The first approach might help when you have to show the toolbar in other ViewControllers that are managed by your NavigationController.
You can subclass UINavigationController and change the Y-axis position of the toolbar when the value is set.
import UIKit
private var context = 0
class NavigationController: UINavigationController {
private var inToolbarFrameChange = false
var observerBag: [NSKeyValueObservation] = []
override func awakeFromNib() {
super.awakeFromNib()
self.inToolbarFrameChange = false
}
override func viewDidLoad() {
super.viewDidLoad()
observerBag.append(
toolbar.observe(\.center, options: .new) { toolbar, _ in
if !self.inToolbarFrameChange {
self.inToolbarFrameChange = true
toolbar.frame = CGRect(
x: 0,
y: self.navigationBar.frame.height + UIApplication.shared.statusBarFrame.height,
width: toolbar.frame.width,
height: toolbar.frame.height
)
self.inToolbarFrameChange = false
}
}
)
}
override func setToolbarHidden(_ hidden: Bool, animated: Bool) {
super.setToolbarHidden(hidden, animated: false)
var rectTB = self.toolbar.frame
rectTB = .zero
}
}
2) You can create your own UIToolbar and add it to view of the UIViewController. Then, you add the constraints to the leading, trailing and the top of the safe area.
import UIKit
final class ViewController: UIViewController {
private let toolbar = UIToolbar()
private let segmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: ["Op 1", "Op 2"])
control.isEnabled = false
return control
}()
override func loadView() {
super.loadView()
setupToolbar()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.hideBorderLine()
}
private func setupToolbar() {
let barItem = UIBarButtonItem(customView: segmentedControl)
toolbar.setItems([barItem], animated: false)
toolbar.isTranslucent = false
toolbar.isOpaque = false
view.addSubview(toolbar)
toolbar.translatesAutoresizingMaskIntoConstraints = false
toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
}
}
private extension UINavigationBar {
func showBorderLine() {
findBorderLine().isHidden = false
}
func hideBorderLine() {
findBorderLine().isHidden = true
}
private func findBorderLine() -> UIImageView! {
return self.subviews
.flatMap { $0.subviews }
.compactMap { $0 as? UIImageView }
.filter { $0.bounds.size.width == self.bounds.size.width }
.filter { $0.bounds.size.height <= 2 }
.first
}
}

Push QLPreviewController and set Translucent to false?

I am unable to disable the translucent property of my QLPreviewController. What i have already tried:
let preview = SideQLPreviewController()
preview.navigationController?.navigationBar.isTranslucent = false //before
self.navigationController?.pushViewController(preview, animated: false)
preview.navigationController?.navigationBar.isTranslucent = false //after
self.navigationController?.navigationBar.isTranslucent = false
And already tried to subclass and set:
class SideQLPreviewController: QLPreviewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isTranslucent = false
// Do any additional setup after loading the view.
}
But still no success - any ideas?
if you present the QLPreviewController there is no navigationcontroller at all. something like this could work though:
class PreviewController: QLPreviewController {
var navigationBar: UINavigationBar? {
return view.recursiveSubviews.filter({ $0 is UINavigationBar }).first as? UINavigationBar
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationBar?.isTranslucent = false
}
}
extension UIView {
var recursiveSubviews: [UIView] {
var recursiveSubviews: [UIView] = []
for subview in subviews {
recursiveSubviews.append(subview)
recursiveSubviews.append(contentsOf: subview.recursiveSubviews)
}
return recursiveSubviews
}
}
You can do it in the viewDidLayoutSubviews of your subclass, this worked for me.
class PreviewController: QLPreviewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
navigationController?.navigationBar.isTranslucent = false
}
}
Then just instantiate the new class
let previewController = PreviewController()//QLPreviewController()
previewController.dataSource = self
navigationController?.pushViewController(previewController, animated: true)

Removing the hairline under Navigation Bar

The effect that I want to achieve is:
And the current state of my app is:
This is the set up of my view controller. I put a tool bar underneath the navigation bar. Then, I set the tool bar's delegate to the navigation bar. I've read several posts about this. One solution that was provided was:
navigationController?.navigationBar.shadowImage = UIImage();
navigationController?.navigationBar.setBackgroundImage(UIImage(), forBarMetrics: .Default)
However, this causes the navigation bar to become white and loses the effect. So I got the following code from this post (UISegmentedControl below UINavigationbar in iOS 7):
#IBOutlet weak var toolbar: UIToolbar!
var hairLine: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
doneButton.enabled = false
for parent in self.navigationController!.navigationBar.subviews {
for childView in parent.subviews {
if childView is UIImageView && childView.bounds.size.width == self.navigationController!.navigationBar.frame.size.width {
hairLine = childView
print(hairLine.frame)
}
}
}
}
func removeHairLine(appearing: Bool) {
var hairLineFrame = hairLine.frame
if appearing {
hairLineFrame.origin.y += toolbar.bounds.size.height
} else {
hairLineFrame.origin.y -= toolbar.bounds.size.height
}
hairLine.frame = hairLineFrame
print(hairLine.frame)
}
override func viewWillAppear(animated: Bool) {
removeHairLine(true)
}
override func viewWillDisappear(animated: Bool) {
removeHairLine(true)
}
However, this code removes the hairline before the view is completely loaded but when the view is loaded, it appears again. Any solutions?
I found solution on this site but don't remember where exactly.
Objective-C:
#interface YourViewController () {
UIImageView *navBarHairlineImageView;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
navBarHairlineImageView = [self findHairlineImageViewUnder:self.navigationController.navigationBar];
navBarHairlineImageView.hidden = YES;
}
- (UIImageView *)findHairlineImageViewUnder:(UIView *)view {
if ([view isKindOfClass:UIImageView.class] && view.bounds.size.height <= 1.0) {
return (UIImageView *)view;
}
for (UIView *subview in view.subviews) {
UIImageView *imageView = [self findHairlineImageViewUnder:subview];
if (imageView) {
return imageView;
}
}
return nil;
}
Swift:
class YourViewController: UIViewController {
var navBarLine: UIImageView?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navBarLine = findHairlineImageViewUnderView(self.navigationController?.navigationBar)
navBarLine?.hidden = true
}
func findHairlineImageViewUnderView(view: UIView?) -> UIImageView? {
if view.isKindOfClass(UIImageView.classForCoder()) && view.bounds.height <= 1 {
return view as? UIImageView
}
for subview in view.subviews {
if let imgView = findHairlineImageViewUnderView(subview) {
return imgView
}
}
return nil
}
}
I use this lines of code
UINavigationBar.appearance().shadowImage = UIImage()
UINavigationBar.appearance().setBackgroundImage(UIImage(named: "background"), for: .default)
Try this
for parent in self.navigationController!.navigationBar.subviews {
for childView in parent.subviews {
if(childView is UIImageView) {
childView.removeFromSuperview()
}
}
}
I hope this help you.
You could use this
self.navigationController?.navigationBar.subviews[0].subviews.filter({$0 is UIImageView})[0].removeFromSuperview()
I didn't find any good Swift 3 solution so I am adding this one, based on Ivan Bruel answer. His solution is protocol oriented, allows to hide hairline in any view controller with just one line of code and without subclassing.
Add this code to your views model:
protocol HideableHairlineViewController {
func hideHairline()
func showHairline()
}
extension HideableHairlineViewController where Self: UIViewController {
func hideHairline() {
findHairline()?.isHidden = true
}
func showHairline() {
findHairline()?.isHidden = false
}
private func findHairline() -> UIImageView? {
return navigationController?.navigationBar.subviews
.flatMap { $0.subviews }
.flatMap { $0 as? UIImageView }
.filter { $0.bounds.size.width == self.navigationController?.navigationBar.bounds.size.width }
.filter { $0.bounds.size.height <= 2 }
.first
}
}
Then make sure view controller which doesn't need hairline conforms to HideableHairlineViewController protocol and call hideHairline().
Swift 4 version of alexandr answer
Step 1: Create property of type UIImageView?
private var navigationBarHairLine: UIImageView?
Step 2: Create findHairlineImageViewUnderView function
This function filters through the view's subviews to find the view with the height of less than or equal to 1pt.
func findHairlineImageViewUnderView(view: UIView?) -> UIImageView? {
guard let view = view else { return nil }
if view.isKind(of: UIImageView.classForCoder()) && view.bounds.height <= 1 {
return view as? UIImageView
}
for subView in view.subviews {
if let imageView = findHairlineImageViewUnderView(view: subView) {
return imageView
}
}
return nil
}
Step 3: Call the created function in ViewWillAppear and pass in the navigationBar. It will return the hairline view which you then set as hidden.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationBarHairLine = findHairlineImageViewUnderView(view: navigationController?.navigationBar)
navigationBarHairLine?.isHidden = true
}
You can subclass UINavigationBar and set the following in initializer (Swift 5):
shadowImage = UIImage()
setBackgroundImage(UIImage(), for: .default) // needed for iOS 10
E.g.:
class CustomNavigationBar: UINavigationBar {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViews()
}
private func setupViews() {
shadowImage = UIImage()
setBackgroundImage(UIImage(), for: .default) // needed for iOS 10
}
}

Resources