LShi
LShi

Reputation: 1502

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

screen shot with UISegmentedControl below the navigation bar

I'm implementing in Playgound a segmented control underneath the navigation bar.

This seems to be a classic problem, which has been asked:

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?

Upvotes: 1

Views: 3391

Answers (3)

Amadeu Cavalcante Filho
Amadeu Cavalcante Filho

Reputation: 2398

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
    }
}

Upvotes: 1

rmaddy
rmaddy

Reputation: 318854

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
    }
}

Upvotes: 1

Nirav Zalavadia
Nirav Zalavadia

Reputation: 74

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;
    }
}

Upvotes: 1

Related Questions