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
}
}
Related
I use UIView as alert view in my app, and i want to show it as banner on top of screen, when device is not connected to internet. So my issue that this view appears under my nav bar, how can i bring it to front ? I've tried to us UIApplication.shared.keyWindow! and add my backgroundView as subview to it, but it causes other issues.
This is my alert view class: I'll provide all class, but my realisation is in show() method.
import Foundation
import UIKit
import SnapKit
class ConnectionAlertView: UIView, UIGestureRecognizerDelegate {
internal var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = Theme.Color.alertLabelBackgroundColor
view.alpha = 0
view.layer.cornerRadius = 15
return view
}()
internal var dismissButton: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "close_icon")
imageView.layer.cornerRadius = 15
imageView.isUserInteractionEnabled = true
return imageView
}()
internal var descriptionTitleLabel: UILabel = {
let label = UILabel()
label.text = "Відсутнє підключення до Інтернету"
label.font = Theme.Font.fontBodyLarge
label.textColor = .white
return label
}()
internal var descriptionLabel: UILabel = {
let label = UILabel()
label.text = "Перевірте налаштування мережі"
label.font = Theme.Font.fontBodyMedium
label.textColor = .white
return label
}()
// MARK: - Private Methods -
internal func layout() {
backgroundView.addSubview(descriptionTitleLabel)
backgroundView.addSubview(descriptionLabel)
backgroundView.addSubview(dismissButton)
descriptionTitleLabel.snp.makeConstraints { make in
make.trailing.equalTo(backgroundView).offset(54)
make.leading.equalTo(backgroundView).offset(16)
make.top.equalTo(backgroundView).offset(12)
}
descriptionLabel.snp.makeConstraints { make in
make.leading.equalTo(descriptionTitleLabel.snp.leading)
make.top.equalTo(descriptionTitleLabel.snp.bottom).offset(4)
}
dismissButton.snp.makeConstraints { make in
make.width.height.equalTo(30)
make.centerY.equalTo(backgroundView)
make.trailing.equalTo(backgroundView).offset(-16)
}
}
internal func configure() {
let tap = UITapGestureRecognizer(target: self, action: #selector(dismiss(sender:)))
tap.delegate = self
dismissButton.addGestureRecognizer(tap)
}
// MARK: - Public Methods -
func show(viewController: UIViewController) {
guard let targetView = viewController.view else { return }
backgroundView.frame = CGRect(x: 10, y: 50, width: targetView.frame.width - 20 , height: 67)
targetView.addSubview(targetView)
layout()
configure()
// show view
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
UIView.transition(with: self.backgroundView, duration: 0.6,
options: .transitionCrossDissolve,
animations: {
self.backgroundView.alpha = 1
})
}
// hide view after 5 sec delay
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UIView.transition(with: self.backgroundView, duration: 1,
options: .transitionCrossDissolve,
animations: {
self.backgroundView.alpha = 0
})
}
}
// MARK: - Objc Methods -
#objc internal func dismiss(sender: UITapGestureRecognizer) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
UIView.transition(with: self.backgroundView, duration: 1,
options: .transitionCrossDissolve,
animations: {
self.backgroundView.alpha = 0
})
}
}
}
My viewController:
class PhoneNumViewController: UIViewController {
let alert = ConnectionAlertView()
private func checkInternetConnection() {
if !NetworkingMonitor.isConnectedToInternet {
log.error("No internet connection!")
alert.show(viewController: self)
}
}
}
Since you have a navigation controller and do not wish to add this view to the window directly, I can offer the following idea which could work.
Your UIViewController is contained with the UINavigationController so if you add the alert to your UIViewController, you will notice it below the UINavigationBar.
You could instead show the alert from your UINavigationController instead with the following changes.
1.
In the func show(viewController: UIViewController) in your class ConnectionAlertView: UIView I changed the following line:
targetView.addSubview(targetView)
to
targetView.addSubview(backgroundView)
This does not directly relate to your issue but seems to be a bug and causes a crash as it seems like you want to add the background view on the target view.
2.
In your class ViewController: UIViewController, when you want to show your alert view, pass the UINavigationController instead like this:
if let navigationController = self.navigationController
{
alert.show(viewController: navigationController)
}
This should give you the desired result I believe (The image and font looks different as I do not have these files but should work fine at your end):
I'm trying to add a child view controller to a parent view controller in a swift ios application, but when I add the child view controller, the activityIndicatorView doesn't appear. What could I be missing?
Here is a snippet that can be tried in a playground:
import PlaygroundSupport
import Alamofire
class LoadingViewController: UIViewController {
private lazy var activityIndicator = UIActivityIndicatorView(style: .gray)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// We use a 0.5 second delay to not show an activity indicator
// in case our data loads very quickly.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) { [weak self] in
self?.activityIndicator.startAnimating()
}
}
}
// methods for adding and removing child view controllers.
extension UIViewController {
func add(_ child: UIViewController, frame: CGRect? = nil) {
addChild(child)
if let frame = frame {
child.view.frame = frame
}
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
// Just to be safe, we check that this view controller
// is actually added to a parent before removing it.
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
class MyViewController : UITabBarController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.view = view
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let loadingViewController = LoadingViewController()
add(loadingViewController, frame: view.frame)
AF.request("http://www.youtube.com").response { response in
print(String(describing: response.response))
loadingViewController.remove()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
The loadingViewController view fills the parent view, and i've tried to change the background colour at different points and that works. but activityIndicator or any other subview i try to add just doesn't appear.
Try add the line
activityIndicator.startAnimating()
in your viewDidLoad() method from your LoadingViewController class
I have a function in my controller I call
private func toggleLauncher() {
let launcher = CommentsLauncher()
launcher.showLauncher()
}
This essentially adds a view on top of the current view, with a semi transparent background.
I'd like to then render a custom inputAccessoryView at the bottom of the newly added view.
class CommentsLauncher: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0, alpha: 0.5)
}
func showLauncher() {
if let window = UIApplication.shared.keyWindow {
window.addSubview(view)
}
}
override var inputAccessoryView: UIView? {
get {
let containerView = UIView()
containerView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
containerView.backgroundColor = .purple
return containerView
}
}
override var canBecomeFirstResponder: Bool {
return true
}
}
All that happens though is the semi transparent background is visible but I see no inputAccessoryView added to the view also and I am unsure why.
Your CommentsLauncher never become the first responder in the code you provided. A UIResponder's inputAccessoryView is displayed when the responder becomes first responder.
Change your showLauncher method to something like this:
func showLauncher() {
if let window = UIApplication.shared.keyWindow {
window.addSubview(view)
becomeFirstResponder()
}
}
And you should see the input accessory view.
I'm working in swift to make an app the uses the cocoapod framework BBSlideoutMenu to display a menu. I am also using the cocoapod framework ChameleonFramework. What I'm trying to do is make the hamburger button that I'm using change colour when it is opened. I haven't yet implemented it, but I also want to make the bar on top transparent. I've recorded it here so you can see what is happening. Basically, the view only gets updated when I slide away the menu.
Disclaimer: I am aware that using a hamburger menu is viewed as bad code design, unfortunately it is what I need in this app.
Here is my code:
import UIKit
import BBSlideoutMenu
import ChameleonFramework
class ViewController: UIViewController, BBSlideoutMenuDelegate {
#IBOutlet var slideMenu: BBSlideoutMenu!
var button: HamburgerButton! = nil
var menuOpen = false;
let screenSize: CGRect = UIScreen.mainScreen().bounds
let topBar = UIView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height * 0.1))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib
view.backgroundColor = UIColor.whiteColor()
topBar.backgroundColor = FlatRed()
button = HamburgerButton(frame: CGRectMake(0, 20, 54, 54))
button.addTarget(self, action: #selector(toggleMenu(_:)), forControlEvents:.TouchUpInside)
topBar.addSubview(button)
view.addSubview(topBar)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
slideMenu.setupEdgePan()
slideMenu.slideDirection = .Right
slideMenu.shrinkAmount = 0
slideMenu.slideTravelPercent = 0.87
slideMenu.menuOffset = 0
slideMenu.zoomFactor = 1
slideMenu.springEnabled = false
slideMenu.backgroundColor = FlatRed()
slideMenu.delegate = self
slideMenu.setupEdgePan()
}
func toggleMenu(sender : HamburgerButton!) {
if(menuOpen) {
slideMenu.dismissSlideMenu(animated: true, time: nil)
} else {
slideMenu.presentSlideMenu(true) { () -> Void in
//Runs after menu is presented
}
}
}
func didPresentBBSlideoutMenu(menu: BBSlideoutMenu) {
menuOpen = true
button.changeColor(UIColor.blackColor())
}
func didDismissBBSlideoutMenu(menu: BBSlideoutMenu) {
menuOpen = false
button.changeColor(UIColor.whiteColor())
}
}
I am using this hamburger menu button, which has been created with CoreGraphics and QuartzCore, and have added the following function for change colour.
func changeColor(color: UIColor) {
for layer in [ self.topStroke, self.middleStroke, self.bottomStroke ] {
layer.fillColor = nil
layer.strokeColor = color.CGColor
layer.lineWidth = 4
layer.miterLimit = 4
layer.lineCap = kCALineCapRound
layer.masksToBounds = true
let strokingPath = CGPathCreateCopyByStrokingPath(layer.path, nil, 4, .Round, .Miter, 4)
layer.bounds = CGPathGetPathBoundingBox(strokingPath)
layer.actions = [
"strokeStart": NSNull(),
"strokeEnd": NSNull(),
"transform": NSNull()
]
self.layer.addSublayer(layer)
}
}
Edit: I have tried using setNeedsDisplay on the button, the topBar, and both of them consecutively in the functions toggleMenu, didPresentBBSlideoutMenu and didDismissBBSlideoutMenu and it didn't work
I have also tried calling it on the actual view (self.view)
Try calling setNeedsDisplay the button.
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;
}