When added UIButton on UITabbar to middle as shown in figure.
The button action on above the UITabBar unable to click
func setupMiddleButton() {
plusButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var menuButtonFrame = plusButton.frame
menuButtonFrame.origin.x = tabBar.bounds.width/2 - menuButtonFrame.size.width/2
let hasNotched :Bool? = UIDevice.current.hasNotch
if hasNotched != nil {
menuButtonFrame.origin.y = tabBar.bounds.height - menuButtonFrame.height - 15
} else {
menuButtonFrame.origin.y = tabBar.bounds.height - menuButtonFrame.height - 50
}
plusButton.frame = menuButtonFrame
plusButton.setTitle("+", for: .normal)
plusButton.titleLabel?.font = UIFont.helveticaNeue(ofSize: 40)
plusButton.backgroundColor = UIColor.init(hexString: "5E71FE")
plusButton.titleEdgeInsets = UIEdgeInsets(top: 0,left: 10,bottom: 10,right: 10)
tabBar.addSubview(plusButton)
plusButton.layer.cornerRadius = menuButtonFrame.height/2
plusButton.addTarget(self, action: #selector(plusButtonAction(sender:)), for: .touchUpInside)
}
You need to override the hitTest method in your custom tab bar class like this
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed()
{
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event)
else { continue }
return result
}
return nil
}
Basically the problem is that upper part is not clickable because it is outside of the bounds of main content view of tab bar.
This method will check if the tap is inside the bounds of the view, if it is it will return the view and the action for that button will get called.
Documentation by apple: Link
P.s I was facing the same issue recently and got this help which worked smooth.
I suspect that what you are trying to do is not possible, or at the least, not supported by Apple. (And thus not recommended since you might find a way to make it might work today but not in some future OS version.)
As a rule, Apple does not support you adding custom view objects to system components like tab bars, navigation bars, stack views, table/collection view controllers, etc except through a documented API.
I would suggest NOT doing what you are trying to do. instead, add a button in the content view of the tab bar controller. I don't know if you'll be able to make it partly cover the tab bar like you are trying to do however.
Add the button to the view of the UITabbarController instead of adding to the TabBar. And then reposition the button, it will work.
Related
After reading a few articles about custom UITabBarControllers, I am left more confused than before I even started doing the research in the first place.
My goal is to create a custom TabBar with 3 important properties:
No text, just icons
The active icon is marked by a circle filled with a color behind it, and therefore needs a different icon color
Here's what I am trying to achieve:
I've been able to remove the text and center the icon by following another StackOverflow answer (Remove tab bar item text, show only image), although the solution seems like a hack to me.
How would I go about creating a circle behind the item and change the active item's color?
Also, would someone mind explaining the difference between the XCode inspector sections "Tab Bar Item" and "Bar Item", which appear directly under each other?
The first step is simple: leaving the title property of the UITabbarItem empty should hide the label.
Your second step can actually be broken down into two steps: changing the color of the icon and adding a circle behind it.
The first step here is simple again: you can set a different icon to use for the currently selected ViewController (I use Storyboards, this process is pretty straightforward). What you'd do is add a white version of the icon to be shown when that menu option is selected.
The final step is displaying the circle. To do this, we'll need the following information:
Which item is currently selected?
What is the position of the icon on the screen?
The first of these two is pretty easy to find out, but the second poses a problem: the icons in a UITabBar aren't spaced around the screen equally, so we can't just divide the width of the tabbar by the amount of items in it, and then take half of that to find the center of the icons. Instead, we will subclass UITabBarController.
Note: the tabBar property of a UITabBarController does have a .selectionIndicatorImage property. You can assign an image to this and it will be shown behind your icon. However, you can't easily control the placement of this image, and that is why we still resort to subclassing UITabBarController.
class CircledTabBarController: UITabBarController {
var circle: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let numberOfItems = CGFloat(tabBar.items!.count)
let tabBarItemSize = CGSize(width: (tabBar.frame.width / numberOfItems) - 20, height: tabBar.frame.height)
circle = UIView(frame: CGRect(x: 0, y: 0, width: tabBarItemSize.height, height: tabBarItemSize.height))
circle?.backgroundColor = .darkGray
circle?.layer.cornerRadius = circle!.frame.width/2
circle?.alpha = 0
tabBar.addSubview(circle!)
tabBar.sendSubview(toBack: circle!)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let index = -(tabBar.items?.index(of: tabBar.selectedItem!)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
circle?.center.x = frame.origin.x + frame.width/2
circle?.alpha = 1
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let index = -(tabBar.items?.index(of: item)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
self.circle?.center.x = frame.origin.x + frame.width/2
}
func frameForTabAtIndex(index: Int) -> CGRect {
var frames = tabBar.subviews.compactMap { (view:UIView) -> CGRect? in
if let view = view as? UIControl {
for item in view.subviews {
if let image = item as? UIImageView {
return image.superview!.convert(image.frame, to: tabBar)
}
}
return view.frame
}
return nil
}
frames.sort { $0.origin.x < $1.origin.x }
if frames.count > index {
return frames[index]
}
return frames.last ?? CGRect.zero
}
}
Now use this subclass of UITabBarController instead of the base class.
So why this approach over simply changing the icon to a circled one? Because you can do many different things with this. I wrote an article about animating the UITabBarController in a similar manner, and if you like, you can easily use above implementation to add animation to yours too.
The easiest and actually cleanest way to do it is to design your icons and import them as images to the .xcassets folder. Then you can just set the different icons for the different states for each of the viewControllers with:
ViewController.tabBarItem = UITabBarItem(title: "", image: yourImage.withRenderingMode(.alwaysOriginal), selectedImage: yourImage)
your selected image will be the one with the circle and the image will be without. It is way easier than manipulating the images in xcode and it is also less expensive since the compiler only has to render the images and doesn't have to manipulate them.
About the other question UIBarItem is
An abstract superclass for items that can be added to a bar that appears at the bottom of the screen.
UITabBarItem is a subclass of UIBarItem to provide extra funtionality.
I've examined many GitHub and StackOverflow solutions to creating a raised center tab bar button and tried multiple times into creating one but I always end of right back to square one.
So below are the steps of what I've been doing, if someone can solve the problem it would be greatly appreciated.
Created a Tab Bar Controller to navigate to 5 different View Controllers
Created a Custom Class for the Tab Bar Controller called CustomTabBarController
Inside my CustomTabBarController File is
import UIKit
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setupMiddleButton()
}
func setupMiddleButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height:64))
var menuButtomFrame = menuButton.frame
menuButtomFrame.origin.y = view.bounds.height - menuButtomFrame.height
menuButtomFrame.origin.x = view.bounds.width/2 - menuButtomFrame.size.width/2
menuButton.frame = menuButtomFrame
view.addSubview(menuButton)
menuButton.setImage(UIImage(named: "MidPhoto"), for: .normal)
menuButton.addTarget(self, action: #selector(menuButtonAction(sender:)), for: .touchUpInside)
view.layoutIfNeeded()
tabBarController?.tabBar.addSubview(menuButton)
}
#objc private func menuButtonAction(sender: UIButton) {
selectedIndex = 2
}
}
That's it. I only edited 1 file (CustomTabBarController) and it works but the image is appearing into unwanted View Controllers and I've done multiple things like: Hide Bottom Bar On Push, self.tabBarController?.tabbar.isHidden = true, and so on.
What can I do to fix this?
I've removed the back button's text by manually setting it to " " on each navigation item, however there is still extra padding between the button and the navigation item's title for no reason.
Does anyone know how to get rid of this annoying spacing?
In a few real case scenarios in my app, the title does concatenate as it gets slightly too long, even though it wouldn't need to if that space were not there.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
let arrayViews = (self.navigationController?.navigationBar.subviews)
if let itemView = arrayViews?[1] {
for lbl in itemView.subviews {
lbl.frame = CGRect(x: -25, y: lbl.frame.origin.y, width: lbl.frame.size.width, height: lbl.frame.size.height)
}
}
}
You should create a custom UIBarButtonItem which uses popToViewController to go back to the previous item on your stack. That way, you can manually set the frame of your custom back button.
Situation: I've got a UITableViewController loading some data asynchronously from a service. During this time I would like to place a full screen (except navigation bar) view over the table view showing my custom indicator and text.
Problem: The problem I'm facing is that when my custom view (it has a red background) is placed over the UITableView the lines of the table view are shown trough my custom view (see image below).
What I tried:
I tried to use insertBelow and above, didn't work. I also tried to do: tableview.Hidden = true, but this also hides the custom view for some reason as seen on image 2.
Image1: For some reason I can see the lines threw my view.
Image 2: Tableview + custom view gone when hidden = true used.
My code:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
UIView view = new UIView (new RectangleF (0, 0, this.TableView.Frame.Width, this.TableView.Frame.Height));
view.BackgroundColor = UIColor.Red;
this.TableView.AddSubview (view);
TableView.Source = new SessionTableViewSource ();
}
You can use self.navigationController.view as view for adding subview.
The issue is that the View of a UITableViewController is a UITableView, so you cannot add subviews to the controller on top of the table.
I'd recommend switching from a UITableViewController to a simple UIViewController that contains a UITableView. This way the controller main view is a plain UIView that contains a table, and you can add subviews to the main UIView and they will be placed on top of the table view.
You can try to add the view to the window instead of nesting it in the table view like this:
UIWindow* mainWindow = [[UIApplication sharedApplication] keyWindow];
[mainWindow addSubview: overlayview];
UIWindow* window = [[UIApplication sharedApplication].delegate.window;
[window addSubview: your-overlayview];
Swift / Storyboard Solution
Note: The code below assumes one has a custom view (ratingView in my case) that is to be presented over a UITableView.
I've read many answers to this and similar questions on SO. The other answers from these sources worked to varying degrees for me (e.g.,view loaded but not shown or not accessible,...). I am using Swift 2.0+ and I am sharing the complete solution for doing this using a UITableViewController.
Create an outlet to the Navigation Bar and the view, which you want to bring over the tableview.
//MARK:Outlets
#IBOutlet weak var navBar:UINavigationBar!
#IBOutlet var ratingView: MNGStarRating!
In my case I also wanted to animate the view over the tableview so I used a class variable to hold a reference to the inflection point and a point above the scene (off-screen).
var centerYInflection:NSLayoutConstraint!
var aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)
Then in viewDidLoad I called a function (configureRatingViewAutoLayout) which configures and adds the constraints for the new view to be animated over the tableview.
func configureRatingViewAutoLayout() {
//REQUIRED
self.navBar.superview?.addSubview(self.ratingView)
var newConstraints:[NSLayoutConstraint] = []
newConstraints.append(self.ratingView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor,constant: 10))
newConstraints.append(self.ratingView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor,constant: 10))
newConstraints.append(self.ratingView.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor))
//hides the rating view above the scene
self.centerYInflection = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor, constant: self.aPointAboveScene)
//the priority must be set below 1000 if you intend to change it after it has been added to a view
self.centerYInflection.priority = 750
newConstraints.append(self.centerYInflection)
//constraints must be added to the container view of the two items
self.ratingView.superview?.addConstraints(newConstraints)
}
Nota Bene - On a UITableViewController; the self.view is the
self.tableView. They point to the same thing so I guess one could also
use the self.tableView reference above.
Sometime later... In response to a UIControl event I call this method.
#IBAction func toggleRatingView (sender:AnyObject?){
//REQUIRED
self.ratingView.superview?.layoutIfNeeded()
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.37, initialSpringVelocity: 0.99, options: [.CurveEaseOut], animations: { () -> Void in
if CGRectContainsRect(self.view.frame, self.ratingView.frame) {
//in frame ~ animate away
//I play a sound to alert the user something is happening
self.centerYInflection.constant = self.aPointAboveScene
self.centerYInflection.priority = UILayoutPriority(950)
//I disable portions of the UI
self.disableUIElements(nil)
} else {
//out of frame ~ animate in
//I play a different sound here
self.centerYInflection.constant = 0
self.centerYInflection.priority = UILayoutPriority(950)
//I enable the UI fully
self.enableUIElements(nil)
}
//REQUIRED
self.ratingView.superview?.setNeedsLayout()
self.ratingView.superview?.layoutIfNeeded()
}) { (success) -> Void in
//do something else
}
}
These helper methods can be configured to control access to elements in your scene during the presentation of the view.
func disableUIElements(sender:AnyObject?) {
//UI
}
func enableUIElements(sender:AnyObject?) {
//UI
}
Caveats
My view is a custom view in the Storyboard (sitting outside of the
tableview but connected to the TableView Controller). The view has a
required user runtime attribute defined layer.zPosition with a Number value set to 2 (this ensures that it presents in front of the
UITableView).
One could also try playing around with bringSubviewToFront:
and sendSubviewToBack: methods if you don't want to set the zPosition
(I think zPosition is simpler to use)
Try this to hook a button at bottom of the UITableViewController
declare button as a variable:
var submitButton: UIButton!
and in viewDidLoad:
submitButton = UIButton(frame: CGRect(x: 5, y: UIScreen.main.bounds.size.height - 50, width: UIScreen.main.bounds.size.width - 10, height: 50))
submitButton.backgroundColor = UIColor.init(red: 180/255, green: 40/255, blue: 56/255, alpha: 1.0)
submitButton.setTitle("Submit", for: .normal)
submitButton.titleLabel?.font = UIFont(name: "Arial", size: 15)
submitButton.titleLabel?.textColor = .white
submitButton.addTarget(self, action: #selector(submit), for: .touchUpInside)
submitButton.layer.cornerRadius = 5
self.view.addSubview(submitButton)
and implement this method:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
submitButton.frame = CGRect.init(x: submitButton.frame.origin.x, y: UIScreen.main.bounds.size.height + scrollView.contentOffset.y - 50, width: submitButton.frame.width, height: submitButton.frame.height)
}
This works for me:
if let myTopView = Bundle.main.loadNibNamed("MyTopView", owner: self, options: nil)?.first as? MyTopView {
if let view = UIApplication.shared.keyWindow{
view.addSubview(myView);
myTopView.translatesAutoresizingMaskIntoConstraints = false
myTopView.topAnchor.constraint(equalTo: view.topAnchor ).isActive = true
myTopView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myTopView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myTopView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}
At the moment I have a standard tab bar, when tapped goes to its corresponding viewController. But I want to make a menu pop out from tab bar when more tab is selected, as shown in below image.
Is any suggestion to implement this?
Thanks in advance.
I would recommend you do so:
First, you should think of all the tab types that could be in tab bar. On your screenshot there are tabs, that present controller, and tab, that presents menu. So we could create enum with all these types:
enum TabType {
case controller
case menu
}
After that you can store array of tab types in order they are shown in tab bar, for your screenshot like so
let tabTypes: [TabType] = [.controller, .controller, .controller, .controller, .menu]
Then you should implement UITabBarControllerDelegate's func tabBarController(_:, shouldSelect:) -> Bool method, which returns true if tab bar is allowed to select the passed controller, and false otherwise.
If you return true than all other work (like presenting view controller and other stuff) tab bar controller will do for you.
In your case you want to execute custom action on tab click, so you should return false. Before returning you should present your menu.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if let index = tabBarController.viewControllers?.index(of: viewController),
index < tabBarTypes.count {
let type = tabBarTypes[index]
switch type {
case .menu:
// perform your menu presenting here
return false
case .controller:
// do nothing, just return true, tab bar will do all work for you
return true
}
}
return true
}
In this implementation you can easily change tab types order or add some another tab type and handle it appropriate.
Its not good UI but although if you want.
First You have to implement delegate method of UITabbarControllerDelegate as below
func tabBarController(_ tabBarController: UITabBarController, shouldSelect
viewController: UIViewController) -> Bool {
if viewController.classForCoder == moreViewController.self
{
let popvc = MoreSubMenuViews(nibName: "MoreSubMenuViews", bundle: Bundle.main)
self.addChildViewController(popvc)
let tabbarHeight = tabBar.frame.height
let estimatedWidth:CGFloat = 200.0
let estimatedHeight:CGFloat = 300.0
popvc.view.frame = CGRect(x:self.view.frame.width - estimatedWidth, y: self.view.frame.height - estimatedHeight - tabbarHeight, width: estimatedWidth, height: estimatedHeight)
self.view.addSubview(popvc.view)
popvc.didMove(toParentViewController: self)
print("sorry stop here")
return true // if you want not to select return false here
}else{
//remove popup logic here if opened
return true
}
}
Here moreViewController is last tab controller & MoreSubMenuViews is nib/xib file which contains buttons shown in you image.
Instead of showing a menu, you could use a scrollable tabs view for a better UI. If you prefer using a library, here's a simple scrollable tab-bar you could implement.
This is an abuse of the UI patterns layout in Apples HIG. Specifically:
Use a tab bar strictly for navigation. Tab bar buttons should not be used to perform actions. If you need to provide controls that act on elements in the current view, use a toolbar instead.
This control should be used to flatten your app hierarchy. It seems that in this case you are mixing functionality of the button. Sometimes it selects a separate view controller, sometimes it displays a action list. This is a confusing situation for users and should be avoided.
A way you could achieve this and still adhere to HIG is by using navigation or tool bars. Imbed this control in a toolbar button. The most simple case would be to invoke a UIActionController with the .actionSheet style selected.
As per my understanding what you want is that, tab 1 is selected and user goes to tab 5 then background will be tab 1 & vice-versa for tab 2,3 & 4 and there should be pop up like buttons as shown in your image. please correct me if I'm wrong.
For this you have to capture the image while user navigate through tabs.
Please find below code in AppDelegate,
var currentImage: UIImage! //It will be public variable
Tabbar delegate methods,
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if tabBarController.selectedIndex == 4 {
let moreVC = tabBarController.selectedViewController as! MoreVC
moreVC.currentImage = currentImage
}
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if tabBarController.selectedIndex != 4 {
let navController = tabBarController.selectedViewController as! UINavigationController
let viewController = navController.viewControllers.last
currentImage = Common.imageWithView(inView: (viewController?.navigationController?.view)!) //UIImage(view: (viewController?.navigationController?.view)!)
}
return true
}
Here I've taken static 4 as last tab index & MoreVC will be the associated ViewController of tab index 4.
Common class to get image from view,
class Common {
static func imageWithView(inView: UIView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(inView.bounds.size, inView.isOpaque, 0.0)
defer { UIGraphicsEndImageContext() }
if let context = UIGraphicsGetCurrentContext() {
inView.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
return nil
}
}
Please find MoreVC,
#IBOutlet weak var imageView: UIImageView!
var currentImage: UIImage! {
didSet {
imageView.image = currentImage
}
}
Please find below image for MoreVC Storyboard. Look it does not contain NavigationController like all other ViewControllers.
Let me know in case of any queries.
1)
Create a "Tab5ViewController" and use this:
static func takeScreenshot(bottomPadding padding: CGFloat = 0) -> UIImage? {
let layer = UIApplication.shared.keyWindow!.layer
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(CGSize(width: layer.frame.size.width, height: layer.frame.size.height - padding), false, scale)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return screenshot
}
Use the screenshot as a background image for your "Tab5ViewController"
2) In your "Tab5ViewController" you can have the stack component (the additional tabs) and then just add/remove child view controllers based on the selection
3)
fix edge cases (like tab selection highlight and so on...)