Show back button without navigation view controller - ios

I have the scheme: UITabBarViewController (with 3 tabs).
In all that tabs I don't want to show navigation menu on top.
And from the first tab, I want to push another view controller from button click that will have "back" button (and top toolbar with "cancel")
I tried some ways - in storyboard with push segue - no back button.
Probably because i don't have navigation view controller, so my navigation stack is empty.
Programmatically:
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "AddCoinTableViewController") as! AddCoinTableViewController
self.present(nextViewController, animated:true, completion:nil)
If I embed tabs in navigation controller, then I have top toolbar (which I don't want).
Any ideas how to make it?

You can't achieve navigation functionality without using UINavigationController. I mean you have to do all animation kind of stuff on your own, and I think that's not a good idea. Rather than that, you can use UINavigationController, and if you don't want to show navigationBar at some viewController, than do as follows for those view controllers.
override func viewWillApear() {
super.viewDidLoad()
self.navigationController?.isNavigationBarHidden = true
}
override func viewWillDisappear(animated: Bool) {
self.navigationController?.isNavigationBarHidden = false
}

You can embed the navigation controller at your first tab controller (or any you want), and hide it at the controllers you don't want on their viewDidLoad like this:
self.navigationController?.isNavigationBarHidden = true
Doing this, you will be able to see the back Button at the controllers you pushed and didn't hide the navigationBar.
Make sure you push the controller using the navigation controller like this:
self.navigationController?.pushViewController(YOUR VIEW CONTROLLER, animated: true)

The below code will allow you to create your own Navigation handling class and have the "push" "pop" animation that UINavigationController has.. You can create a new project, copy paste the below into ViewController.swift and see for yourself..
Now you can give any UIViewController navigation controller abilities..
import UIKit
class NavigationHandler : NSObject, UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning, UINavigationBarDelegate {
private var isPresenting: Bool = false
private weak var controller: UIViewController?
init(controller: UIViewController) {
super.init()
self.controller = controller
controller.transitioningDelegate = self
let navigationBar = UINavigationBar()
controller.view.addSubview(navigationBar)
NSLayoutConstraint.activate([
navigationBar.leftAnchor.constraint(equalTo: controller.view.leftAnchor),
navigationBar.rightAnchor.constraint(equalTo: controller.view.rightAnchor),
navigationBar.topAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.topAnchor)
])
navigationBar.translatesAutoresizingMaskIntoConstraints = false
navigationBar.delegate = self
let item = UINavigationItem(title: controller.title ?? "")
let barButton = UIBarButtonItem(title: "Back", style: .done, target: self, action: #selector(onBackButton(button:)))
item.leftBarButtonItems = [barButton]
navigationBar.setItems([item], animated: true)
}
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
#objc
private func onBackButton(button: UIBarButtonItem) {
self.controller?.dismiss(animated: true, completion: nil)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.isPresenting = true;
return self;
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.isPresenting = false;
return self;
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.25;
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let duration = self.transitionDuration(using: transitionContext)
let fromController = transitionContext.viewController(forKey: .from)
let toController = transitionContext.viewController(forKey: .to)
let containerView = transitionContext.containerView
if self.isPresenting {
let frame = fromController!.view.frame
containerView.addSubview(toController!.view)
toController?.view.frame = CGRect(x: frame.origin.x + frame.width, y: frame.origin.y, width: frame.width, height: frame.height)
UIView.animate(withDuration: duration, animations: {
fromController?.view.frame = CGRect(x: frame.origin.x - frame.size.width, y: frame.origin.y, width: frame.size.width, height: frame.size.height)
toController?.view.frame = frame
}, completion: { (completed) in
transitionContext.completeTransition(true)
})
}
else {
let frame = fromController!.view.frame
containerView.insertSubview(toController!.view, at: 0)
toController?.view.frame = CGRect(x: frame.origin.x - frame.size.width, y: frame.origin.y, width: frame.size.width, height: frame.size.height)
UIView.animate(withDuration: duration, animations: {
fromController?.view.frame = CGRect(x: frame.origin.x + frame.width, y: frame.origin.y, width: frame.width, height: frame.height)
toController?.view.frame = frame
}, completion: { (completed) in
transitionContext.completeTransition(true)
})
}
}
}
View Controllers for testing:
class ViewController : UIViewController {
private var navigationHandler: NavigationHandler?
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .custom)
button.setTitle("Push Controller", for: .normal)
button.setTitleColor(UIColor.red, for: .normal)
button.layer.borderColor = UIColor.black.cgColor
button.layer.borderWidth = 1.0
button.layer.cornerRadius = 5.0
button.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
button.heightAnchor.constraint(equalToConstant: 45.0),
button.widthAnchor.constraint(equalToConstant: 150.0)
])
button.addTarget(self, action: #selector(onPush(button:)), for: .touchUpInside)
}
#objc
private func onPush(button: UIButton) {
let child = ChildViewController()
self.navigationHandler = NavigationHandler(controller: child)
self.present(child, animated: true, completion: nil)
}
}
class ChildViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.blue
}
}

Related

How to get rounded corner & dynamic height of popped custom modalPresentationStyle VC

When present a VC, the default style is not cover full screen and with a rounded corner like below gif illustrated.
But I want to control the height of modalPresentation, let's say a 1/4 screen height by default and dynamic changed according to tableView's rows of popped VC. So I implement a custom modalPresentationStyle on the base VC wit below code.
However, I found these issues after:
The popped VC is not rounded corner but with rectangle corner.
I cannot drag to move the popped VC anymore, it is in fixed position.
It would be more better if I could to increment the popped VC's height per its tableView rows count. Not a must item.
#objc func collectButtonTapped(_ sender: Any?) {
let vc = PlayListViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = self
present(vc, animated: true)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return HalfSizePresentationController(presentedViewController: presented, presenting: presentingViewController)
}
class HalfSizePresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
guard let bounds = containerView?.bounds else { return .zero }
return CGRect(x: 0, y: bounds.height * 0.75, width: bounds.width, height: bounds.height * 0.75)
}
}
Try to assign cornerRadius to your vc:
#objc func collectButtonTapped(_ sender: Any?) {
let vc = PlayListViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = self
// assign corner radius
vc.view.layer.cornerRadius = 20
vc.view.clipsToBounds = true
vc.view.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] // this is for corner radius only for top
present(vc, animated: true)
}
for total control of vc presented position, you can use child vc and auto layout, for present child vc (like a modal presentation style) you can use UIView.animate on child vc top constraint.
this is an example of child vc and auto layout:
import UIKit
class YourController: UIViewController {
private lazy var firstChildVc = AiutiFirst()
let myButton: UIButton = {
let b = UIButton(type: .system)
b.layer.cornerRadius = 10
b.clipsToBounds = true
b.backgroundColor = .black
b.setTitleColor(.white, for: .normal)
b.setTitle("Present", for: .normal)
b.addTarget(self, action: #selector(handlePresent), for: .touchUpInside)
b.translatesAutoresizingMaskIntoConstraints = false
return b
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
addChildVC()
}
var up = false
#objc fileprivate func handlePresent() {
print("present")
if up == false {
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
self.menuDown?.isActive = false
self.menuUp?.isActive = true
self.myButton.setTitle("Dismiss", for: .normal)
self.view.layoutIfNeeded()
} completion: { _ in
print("Animation completed")
self.up = true
}
} else {
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
self.menuUp?.isActive = false
self.menuDown?.isActive = true
self.myButton.setTitle("Present", for: .normal)
self.view.layoutIfNeeded()
} completion: { _ in
print("Animation completed")
self.up = false
}
}
}
var menuUp: NSLayoutConstraint?
var menuDown: NSLayoutConstraint?
fileprivate func addChildVC() {
addChild(firstChildVc)
firstChildVc.view.translatesAutoresizingMaskIntoConstraints = false
firstChildVc.view.layer.cornerRadius = 20
firstChildVc.view.clipsToBounds = true
firstChildVc.view.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] // this is for corner radius only for top
view.addSubview(firstChildVc.view)
menuUp = firstChildVc.view.topAnchor.constraint(equalTo: view.centerYAnchor)
menuDown = firstChildVc.view.topAnchor.constraint(equalTo: view.bottomAnchor)
menuDown?.isActive = true
firstChildVc.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
firstChildVc.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
firstChildVc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
firstChildVc.didMove(toParent: self)
view.addSubview(myButton)
myButton.bottomAnchor.constraint(equalTo: view.centerYAnchor, constant: -40).isActive = true
myButton.widthAnchor.constraint(equalToConstant: 200).isActive = true
myButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
myButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
}
And this is the result:
To animate child vc presentation you can use UIView.animate function for top child vc constraint, or Pan gesture do drag it, or whatever you deem necessary and valid to use...
to show it full screen simple set child vc top anchor to top of intere view:
menuUp = firstChildVc.view.topAnchor.constraint(equalTo: view.topAnchor)

currentContext presentation style changes the size of the child view controller's view

I have a parent view controller and a child view controller that takes up a portion of the parent's main view:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let childVC = ChildVC()
addChild(childVC)
childVC.view.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 200, height: 200))
view.addSubview(childVC.view)
childVC.didMove(toParent: self)
}
}
class ChildVC: UIViewController {
override func loadView() {
let v = UIView()
v.backgroundColor = .cyan
view = v
}
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .system)
button.setTitle("Present", for: .normal)
button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
button.sizeToFit()
view.addSubview(button)
}
#objc func pressed() {
self.definesPresentationContext = true
self.providesPresentationContextTransitionStyle = true
self.modalTransitionStyle = .crossDissolve
let pvc = PresentedVC()
pvc.modalPresentationStyle = .currentContext
self.present(pvc, animated: true, completion: nil)
}
}
class PresentedVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
let button = UIButton(type: .system)
button.setTitle("Dismiss", for: .normal)
button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
button.sizeToFit()
view.addSubview(button)
}
#objc func pressed() {
self.dismiss(animated: true, completion: nil)
}
}
When I present a view controller using the currentContext style, it presents the new view controller as it should, covering only the child view controller's view:
However, when I dismiss it, the size of the ChildVC's main view takes up the entire screen:
When I log the superview of the ChildVC's main view, it's still a subview of the parent view controller's main view so I'm not sure why this is happening.
Just change your presentationStyle to .overCurrentContext like this:
#objc func pressed() {
self.definesPresentationContext = true
self.providesPresentationContextTransitionStyle = true
self.modalTransitionStyle = .crossDissolve
let pvc = PresentedVC()
pvc.modalPresentationStyle = .overCurrentContext // <-- here
pvc.modalTransitionStyle = .crossDissolve // <-- and here
self.present(pvc, animated: true, completion: nil)
}
It will prevent the unwanted upscaling.

How to dismiss a stack of UIViewControllers to a certain Root ViewController?

Some RootViewController presents a ParentViewController which than presents a ChildViewController.
How can I dissmiss the ChildViewController animated directly to the RootViewController whithout showing the ParentViewController again?
In Detail
Assume some presents the ParentViewController which lets the user enter some credentials to login to some user account.
Once the connection is established the ParentViewController presents the ChildViewController which shows connection / account details to the user
When the user closes the ChildViewController it should be dismissed animated (slide down, etc.). But instead of returning to the ParentViewController the user should get back directly to the RootViewController
Of course it would be possible, that the ParentViewController does not present the ChildViewController itself but (somehow) tells the RootViewController to this. This way it would be no problem to directly return from the ChildViewController to the RootViewController. However, this is NOT what I am looking for, since the RootViewController should not know about the ChildViewController or even care if the ParentViewController presents other VCs.
I am looking for a solution where the ParentViewController controls whether itself is shown after the VC it presented is dismissed or its parent (= the root VC).
Code:
typealias CompletionBlock = () -> Void
class RootViewController: UIViewController {
#IBAction func showParentVC(_ sender: Any) {
let parentVC = ParentViewController()
parentVC.completion = {
self.dismiss(animated: true, completion: nil)
}
present(parentVC, animated: true)
}
}
class ParentViewController: UIViewController {
var completion: CompletionBlock?
#IBAction func showChild(_ sender: Any) {
let childVC = ChildViewController()
childVC.completion = {
self.completion?()
}
present(childVC, animated: true)
}
}
class ChildViewController: UIViewController {
var completion: CompletionBlock?
#IBAction func close(_ sender: Any) {
completion?()
}
}
Using this code does NOT solve the described problem. If close is called on the ChildViewController the RootViewController calls self.dismiss(animated: true, completion: nil). This way the ChildViewController animates away and the ParentViewController becomes visible. Then the ParentViewController animates away and the RootViewControllerbecomes visible.
How to skip the ParentViewController and directly show the RootViewController after animating away the ChildViewController?
My recommendation is embed your RootViewController into a NavigationController (if you don't have it yet) and present both parent a child with
navigationController?.present(viewController, animated: true, completion: nil)
//instead of viewController.present(...)
And then youcan use this method from your childViewController
navigationController?.popToRootViewController(animated: true)
One approach would be to set the view's alpha to Zero when you present another VC onto the "stack" of presented VCs.
So, present the first modal VC from the "root" VC as normal. For each "child" that presents another VC, use:
present(vc, animated: true, completion: {
self.view.alpha = 0.0
})
Now, when you call-back to the "root" VC to dismiss all the VCs, you won't see the partial / flash of the intermediate VC / VCs.
Here is a complete example to test. No #IBOutlets or #IBActions ... just start with a black view controller and assign its Custom Class to MultiPresentDismissViewController:
import UIKit
class MultiPresentDismissViewController: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.font = UIFont.boldSystemFont(ofSize: 40)
v.backgroundColor = .yellow
v.text = "\"Root\" VC"
return v
}()
let showAnotherButton: UIButton = {
let v = UIButton()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.setTitle("Present a VC", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.cyan, for: .highlighted)
return v
}()
let theStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 32
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
[theLabel, showAnotherButton].forEach {
theStackView.addArrangedSubview($0)
$0.layer.borderWidth = 1.0
}
theStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(theStackView)
NSLayoutConstraint.activate([
theStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
])
showAnotherButton.addTarget(self, action: #selector(presentAnotherVC), for: .touchUpInside)
}
#objc func presentAnotherVC() -> Void {
let vc = AnotherViewController()
vc.myID = 1
present(vc, animated: true, completion: nil)
}
}
class AnotherViewController: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.font = UIFont.boldSystemFont(ofSize: 100)
v.backgroundColor = .yellow
return v
}()
let showAnotherButton: UIButton = {
let v = UIButton()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.setTitle("Present Another VC", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.cyan, for: .highlighted)
return v
}()
let defaultDismissButton: UIButton = {
let v = UIButton()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.setTitle("Default Dismiss All", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.cyan, for: .highlighted)
return v
}()
let theStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 20
return v
}()
var myID: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .random()
[theLabel, showAnotherButton, defaultDismissButton].forEach {
theStackView.addArrangedSubview($0)
$0.layer.borderWidth = 1.0
}
theStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(theStackView)
NSLayoutConstraint.activate([
theStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
])
theLabel.text = "\(myID)"
showAnotherButton.addTarget(self, action: #selector(presentAnotherVC), for: .touchUpInside)
defaultDismissButton.addTarget(self, action: #selector(defaultDismissAll), for: .touchUpInside)
}
#objc func presentAnotherVC() -> Void {
let vc = AnotherViewController()
vc.myID = myID + 1
present(vc, animated: true, completion: {
self.view.alpha = 0.0
})
}
#objc func defaultDismissAll() -> Void {
// walk up the "presenting" hierarchy to find the "root" VC
var vc = self.presentingViewController
while vc?.presentingViewController != nil {
vc = vc?.presentingViewController
}
vc?.dismiss(animated: true, completion: nil)
}
}
extension CGFloat {
static func random() -> CGFloat {
return CGFloat(arc4random()) / CGFloat(UInt32.max)
}
}
extension UIColor {
static func random() -> UIColor {
return UIColor(red: .random(),
green: .random(),
blue: .random(),
alpha: 1.0)
}
}

Swift 3 - Push a new view but keep the same NavigationBar

I have a UISearchBar in my application. When I press a button to "open" the searchbar does a new view appear. But the problem is that the NavigationController changes and the UISearchBar disappear. How can I do so I can keep the current NavigationController with my searchbar even if a new view appear. (So I still searching when the new view appear)
P.s my code is not the best and I´m not using Storyboard!
class HomeController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor.white
setupNavigationBar()
}
Here is the new view that appear:
class UserSearchController: UICollectionViewController, UISearchBarDelegate {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor.blue
}
}
And here is the whole searchbar code:
import UIKit
var searchBar = UISearchBar()
var searchBarButtonItem: UIBarButtonItem?
var logoImageView: UIImageView!
extension HomeController {
func setupNavigationBar() {
let button = UIButton(type: .system)
button.setImage(#imageLiteral(resourceName: "search"), for: .normal)
button.addTarget(self, action: #selector(showSearchBar), for: .touchUpInside)
button.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
let barButton = UIBarButtonItem(customView: button)
self.navigationItem.rightBarButtonItem = barButton
let logoImage = UIImage(named: "home")!
logoImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: logoImage.size.width, height: logoImage.size.height))
logoImageView.image = logoImage
navigationItem.titleView = logoImageView
searchBar.delegate = self
searchBar.searchBarStyle = UISearchBarStyle.minimal
searchBar.placeholder = "Search"
searchBar.barTintColor = UIColor.gray
searchBarButtonItem = navigationItem.rightBarButtonItem
}
func showSearchBar() {
let layout = UICollectionViewFlowLayout()
let userSearchController = UserSearchController(collectionViewLayout: layout)
self.navigationController?.pushViewController(userSearchController, animated: true)
searchBar.alpha = 0
navigationItem.titleView = searchBar
navigationItem.setLeftBarButton(nil, animated: true)
navigationItem.rightBarButtonItem = nil
searchBar.showsCancelButton = true
UIView.animate(withDuration: 0.5, animations: {
self.searchBar.alpha = 1
}, completion: { finished in
self.searchBar.becomeFirstResponder()
})
}
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
hideSearchBar()
}
func hideSearchBar() {
navigationItem.setRightBarButton(searchBarButtonItem, animated: true)
logoImageView.alpha = 0
UIView.animate(withDuration: 0.3, animations: {
self.navigationItem.titleView = self.logoImageView
self.logoImageView.alpha = 1
}, completion: { finished in
})
}
}
The reason why your search bar is only visible on your first View Controller is because you are using the View Controller's titleView property. Each UIViewController has it's own titleView property, so if you push a View Controller onto your first VC, it will also need to have the titleView property set to a search bar view with the required configuration.
I think you can create a base class, add UISearchBar above the base class, and then you want the current controller with UISearchBar to inherit your base class

Make custom button on Tab Bar rounded

Here is what I am trying to do:
Note: The screenshot is taken from an earlier version of iOS
What I have been able to achieve:
Code:
override func viewWillAppear(animated: Bool) {
// Creates image of the Button
let imageCameraButton: UIImage! = UIImage(named: "cameraIcon")
// Creates a Button
let cameraButton = UIButton(type: .Custom)
// Sets width and height to the Button
cameraButton.frame = CGRectMake(0.0, 0.0, imageCameraButton.size.width, imageCameraButton.size.height);
// Sets image to the Button
cameraButton.setBackgroundImage(imageCameraButton, forState: .Normal)
// Sets the center of the Button to the center of the TabBar
cameraButton.center = self.tabBar.center
// Sets an action to the Button
cameraButton.addTarget(self, action: "doSomething", forControlEvents: .TouchUpInside)
// Adds the Button to the view
self.view.addSubview(cameraButton)
}
I did try to create a rounded button in the normal way, but this was the result:
Code Snippet for rounded button:
//Creation of Ronded Button
cameraButton.layer.cornerRadius = cameraButton.frame.size.width/2
cameraButton.clipsToBounds = true
Solution
You need to subclass UITabBarController and then add the button above TabBar's view. A button action should trigger UITabBarController tab change by setting selectedIndex.
Code
The code below only is a simple approach, however for a full supporting iPhone (including X-Series)/iPad version you can check the full repository here: EBRoundedTabBarController
class CustomTabBarController: UITabBarController {
// MARK: - View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
let controller1 = UIViewController()
controller1.tabBarItem = UITabBarItem(tabBarSystemItem: .contacts, tag: 1)
let nav1 = UINavigationController(rootViewController: controller1)
let controller2 = UIViewController()
controller2.tabBarItem = UITabBarItem(tabBarSystemItem: .contacts, tag: 2)
let nav2 = UINavigationController(rootViewController: controller2)
let controller3 = UIViewController()
let nav3 = UINavigationController(rootViewController: controller3)
nav3.title = ""
let controller4 = UIViewController()
controller4.tabBarItem = UITabBarItem(tabBarSystemItem: .contacts, tag: 4)
let nav4 = UINavigationController(rootViewController: controller4)
let controller5 = UIViewController()
controller5.tabBarItem = UITabBarItem(tabBarSystemItem: .contacts, tag: 5)
let nav5 = UINavigationController(rootViewController: controller5)
viewControllers = [nav1, nav2, nav3, nav4, nav5]
setupMiddleButton()
}
// MARK: - Setups
func setupMiddleButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var menuButtonFrame = menuButton.frame
menuButtonFrame.origin.y = view.bounds.height - menuButtonFrame.height
menuButtonFrame.origin.x = view.bounds.width/2 - menuButtonFrame.size.width/2
menuButton.frame = menuButtonFrame
menuButton.backgroundColor = UIColor.red
menuButton.layer.cornerRadius = menuButtonFrame.height/2
view.addSubview(menuButton)
menuButton.setImage(UIImage(named: "example"), for: .normal)
menuButton.addTarget(self, action: #selector(menuButtonAction(sender:)), for: .touchUpInside)
view.layoutIfNeeded()
}
// MARK: - Actions
#objc private func menuButtonAction(sender: UIButton) {
selectedIndex = 2
}
}
Output
Swift 3 Solution
With a slight adjustment to EricB's solution to have this work for Swift 3, the menuButton.addTarget() method needs to have it's selector syntax changed a bit.
Here is the new menuButton.addTarget() function:
menuButton.addTarget(self, action: #selector(MyTabBarController.menuButtonAction), for: UIControlEvents.touchUpInside)
When defining my TabBarController class, I also add a UITabBarControllerDelegate and placed all of the that in the
override func viewDidAppear(_ animated: Bool) { ... }
For extra clarity, the full code is:
Full Code Solution
import UIKit
class MyTabBarController: UITabBarController, UITabBarControllerDelegate {
// View Did Load
override func viewDidLoad() {
super.viewDidLoad()
}
// Tab Bar Specific Code
override func viewDidAppear(_ animated: Bool) {
let controller1 = UIViewController(self.view.backgroundColor = UIColor.white)
controller1.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarSystemItem.contacts, tag: 1)
let nav1 = UINavigationController(rootViewController: controller1)
let controller2 = UIViewController()
controller2.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarSystemItem.contacts, tag: 2)
let nav2 = UINavigationController(rootViewController: controller2)
let controller3 = UIViewController()
let nav3 = UINavigationController(rootViewController: controller3)
nav3.title = ""
let controller4 = UIViewController()
controller4.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarSystemItem.contacts, tag: 4)
let nav4 = UINavigationController(rootViewController: controller4)
let controller5 = UIViewController()
controller5.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarSystemItem.contacts, tag: 5)
let nav5 = UINavigationController(rootViewController: controller5)
self.viewControllers = [nav1, nav2, nav3, nav4, nav5]
self.setupMiddleButton()
}
// TabBarButton – Setup Middle Button
func setupMiddleButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var menuButtonFrame = menuButton.frame
menuButtonFrame.origin.y = self.view.bounds.height - menuButtonFrame.height
menuButtonFrame.origin.x = self.view.bounds.width / 2 - menuButtonFrame.size.width / 2
menuButton.frame = menuButtonFrame
menuButton.backgroundColor = UIColor.red
menuButton.layer.cornerRadius = menuButtonFrame.height/2
self.view.addSubview(menuButton)
menuButton.setImage(UIImage(named: "example"), for: UIControlState.normal)
menuButton.addTarget(self, action: #selector(MyTabBarController.menuButtonAction), for: UIControlEvents.touchUpInside)
self.view.layoutIfNeeded()
}
// Menu Button Touch Action
func menuButtonAction(sender: UIButton) {
self.selectedIndex = 2
// console print to verify the button works
print("Middle Button was just pressed!")
}
}
This is the customTabbarcontroller class which is the subclass of UITabbarcontroller. It's the same idea as given by #EridB. But in his code #Raymond26's issue wasn't solved. So, posting a complete solution written in Swift 3.0
protocol CustomTabBarControllerDelegate
{
func customTabBarControllerDelegate_CenterButtonTapped(tabBarController:CustomTabBarController, button:UIButton, buttonState:Bool);
}
class CustomTabBarController: UITabBarController, UITabBarControllerDelegate
{
var customTabBarControllerDelegate:CustomTabBarControllerDelegate?;
var centerButton:UIButton!;
private var centerButtonTappedOnce:Bool = false;
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews();
self.bringcenterButtonToFront();
}
override func viewDidLoad()
{
super.viewDidLoad()
self.delegate = self;
self.tabBar.barTintColor = UIColor.red;
let dashboardVC = DashboardViewController()
dashboardVC.tabBarItem = UITabBarItem(tabBarSystemItem: .topRated, tag: 1)
let nav1 = UINavigationController(rootViewController: dashboardVC)
let myFriendsVC = MyFriendsViewController()
myFriendsVC.tabBarItem = UITabBarItem(tabBarSystemItem: .featured, tag: 2)
let nav2 = UINavigationController(rootViewController: myFriendsVC)
let controller3 = UIViewController()
let nav3 = UINavigationController(rootViewController: controller3)
nav3.title = ""
let locatorsVC = LocatorsViewController()
locatorsVC.tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 4)
let nav4 = UINavigationController(rootViewController: locatorsVC)
let getDirectionsVC = GetDirectionsViewController()
getDirectionsVC.tabBarItem = UITabBarItem(tabBarSystemItem: .history, tag: 5)
let nav5 = UINavigationController(rootViewController: getDirectionsVC)
viewControllers = [nav1, nav2, nav3, nav4, nav5]
self.setupMiddleButton()
}
// MARK: - TabbarDelegate Methods
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController)
{
switch viewController
{
case is DashboardViewController:
self.showCenterButton()
case is MyFriendsViewController:
self.showCenterButton()
case is GetDirectionsViewController:
self.showCenterButton()
case is LocatorsViewController:
self.showCenterButton()
default:
self.showCenterButton()
}
}
// MARK: - Internal Methods
#objc private func centerButtonAction(sender: UIButton)
{
// selectedIndex = 2
if(!centerButtonTappedOnce)
{
centerButtonTappedOnce=true;
centerButton.setImage(UIImage(named: "ic_bullseye_white"), for: .normal)
}
else
{
centerButtonTappedOnce=false;
centerButton.setImage(UIImage(named: "ic_bullseye_red"), for: .normal)
}
customTabBarControllerDelegate?.customTabBarControllerDelegate_CenterButtonTapped(tabBarController: self,
button: centerButton,
buttonState: centerButtonTappedOnce);
}
func hideCenterButton()
{
centerButton.isHidden = true;
}
func showCenterButton()
{
centerButton.isHidden = false;
self.bringcenterButtonToFront();
}
// MARK: - Private methods
private func setupMiddleButton()
{
centerButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var centerButtonFrame = centerButton.frame
centerButtonFrame.origin.y = view.bounds.height - centerButtonFrame.height
centerButtonFrame.origin.x = view.bounds.width/2 - centerButtonFrame.size.width/2
centerButton.frame = centerButtonFrame
centerButton.backgroundColor = UIColor.red
centerButton.layer.cornerRadius = centerButtonFrame.height/2
view.addSubview(centerButton)
centerButton.setImage(UIImage(named: "ic_bullseye_red"), for: .normal)
centerButton.setImage(UIImage(named: "ic_bullseye_white"), for: .highlighted)
centerButton.addTarget(self, action: #selector(centerButtonAction(sender:)), for: .touchUpInside)
view.layoutIfNeeded()
}
private func bringcenterButtonToFront()
{
print("bringcenterButtonToFront called...")
self.view.bringSubview(toFront: self.centerButton);
}
}
This is the DashboardViewController for complete reference:
class DashboardViewController: BaseViewController, CustomTabBarControllerDelegate
{
override func viewDidLoad()
{
super.viewDidLoad()
(self.tabBarController as! CustomTabBarController).customTabBarControllerDelegate = self;
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated);
(self.tabBarController as! CustomTabBarController).showCenterButton();
}
override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated);
self.hidesBottomBarWhenPushed = false;
(self.tabBarController as! CustomTabBarController).hideCenterButton();
}
override func viewWillLayoutSubviews()
{
super.viewWillLayoutSubviews();
if(!isUISetUpDone)
{
self.view.backgroundColor = UIColor.lightGray
self.title = "DASHBOARD"
self.prepareAndAddViews();
self.isUISetUpDone = true;
}
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
//MARK: CustomTabBarControllerDelegate Methods
func customTabBarControllerDelegate_CenterButtonTapped(tabBarController: CustomTabBarController, button: UIButton, buttonState: Bool)
{
print("isDrive ON : \(buttonState)");
}
//MARK: Internal Methods
func menuButtonTapped()
{
let myFriendsVC = MyFriendsViewController()
myFriendsVC.hidesBottomBarWhenPushed = true;
self.navigationController!.pushViewController(myFriendsVC, animated: true);
}
//MARK: Private Methods
private func prepareAndAddViews()
{
let menuButton = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
menuButton.titleLabel?.text = "Push"
menuButton.titleLabel?.textColor = UIColor.white
menuButton.backgroundColor = UIColor.red;
menuButton.addTarget(self, action: #selector(DashboardViewController.menuButtonTapped), for: .touchUpInside)
self.view.addSubview(menuButton);
}
}
with StoryBoard:
Click the tab bar button within the view controller of the particular tab bar item you want to make prominent,
Remove the text, just set the image inset top to -25 of the tab bar button.
Check Like Below image

Resources