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...)
Related
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.
I'm working on an app that displays a today extension with some information. When I tap on the today extension, it opens the app and navigates to a subview from the root to display the information. Normally the user would then click the back arrow to go back to the main view, but there is no way to tell if this is actually done. It is possible for the user to go back to the today extension and tap again. When this is done, the subview is opened once again with new information. If this is done a bunch of times, I end up with a bunch of instances of the subview and I have to click the back button on each of them to get back to the main view.
My question: Is it possible to check if the subview is already visible? I'd like to be able to just send updated information to it, instead of having to display an entirely new view.
I am currently handling this by keeping the instance of the UIViewController at the top of my root. If it is not nil, then I just pass the information to it and redraw. If it is nil, then I call performSegue and create a new one.
I just think that there must be a better way of handling this.
Edit: Thanks to the commenter below, I came up with this code that seems to do what I need.
if let quoteView = self.navigationController?.topViewController as? ShowQuoteVC {
quoteView.updateQuoteInformation(usingQuote: QuoteService.instance.getQuote(byQuoteNumber: quote))
}
else {
performSegue(withIdentifier: "showQuote", sender: quote)
}
This is different from the suggested post where the answer is:
if (self.navigationController.topViewController == self) {
//the view is currently displayed
}
In this case, it didn't work because I when I come in to the app from the Today Extension, it goes to the root view controller. I needed to check whether a subview is being displayed, and self.navigationController.topViewcontroller == self will never work because I am not checking to see if the top view controller is the root view controller. The suggestions in this post are more applicable to what I am trying to accomplish.
u can use this extension to check for currently displayed through the UIApplication UIViewController:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
and usage example:
if let topController = UIApplication.topViewController() {
if !topController.isKind(of: MainViewController.self) { //MainViewController- the controller u wish to equal its type
// do action...
}
}
I'd like to use the NSTabViewController for switching through 6 different Tabs with the toolbar style.
All tabs have in common that they show different aspects of a Customer entity.
Now I want to add aditional NSToolbarItems to the toolbar of the NSTabViewController? But I haven't found a way to access the toolbar.
I also would like to add Space between the ToolbarItems.
Is there a way to do so?
Or how can I add my ViewController from the Storyboard to a NSTabView without using NSTabViewController?
Regards
Oliver
In the meantime I've tried another approach that I thought was more promising but lead to another strange behaviour:
I've created a new NSViewController and put a NSTabView inside. In order to load my already existing ViewControllers I used this
override func viewDidLoad() {
super.viewDidLoad()
let customerController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("CustomerVCID")) as! CustomerViewController
let servicesController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("ServicesVCID")) as! ServicesController
customerController.customer = self.customer
servicesController.customer = self.customer
self.tabView.tabViewItems[0].view = customerController.view
self.tabView.tabViewItems[1].view = servicesController.view
}
That indeed worked, but now all my NSButtons that have actions will cause my application to crash.
There is only one toolbar per window. So your NSTabViewController shares it.
Select toolbar mode of NSTabViewController
Override NSWindowController and add your items
Example:
override func windowDidLoad() {
super.windowDidLoad()
window?.toolbar?.insertItem(withItemIdentifier: .print, at: 0)
}
You can always access your toolbar via following path view->window->toolbar
Your only issue is that there is one delegate per NSToolbar. Which means you have to create your custom NSToolbarItem inside NSTabViewController delegate.
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == .export {
return ExportToolbarItem.new()
} else {
return super.toolbar(toolbar, itemForItemIdentifier: itemIdentifier, willBeInsertedIntoToolbar: flag)
}
}
Remember your are required to call super. This is because underlying method wants to create bindings to view controller.
In case you need actionable buttons in toolbar just add them without calling super.
First of all, I'm a newbie to Swift so apologies if I'm missing something obvious or using the wrong terminology.
Objective: set tab bar item selected image to custom image.
The following setup works (selected item is custom image):
| UITabBarController | => | UIViewController | (setup w/ storyboard)
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let customSelectedImage = UIImage (named: "selected-image")?.withRenderingMode(.alwaysOriginal)
self.tabBarItem.selectedImage = customSelectedImage
}
}
But this setup doesn't work (selected item has default blue tint):
| UITabBarController | => | UINavigationController | => | UIViewController | (setup w/ storyboard - see here)
Similar code to above but added (programmatically) UICollectionView subview to UIViewController.
class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
let customSelectedImage = UIImage (named: "selected-image")?.withRenderingMode(.alwaysOriginal)
self.tabBarItem.selectedImage = customSelectedImage
...
//Some UICollectionView related code
...
}
}
Some things that may be helpful:
In debug session (see print screen) => View UI hierarchy: the selected item (marked as of class UITabBarSwappableImageView) has the correct custom image but the tint is default blue. I tried with different custom images and looks as if they're hidden by another (default ?) view...
If I change the UITabBar.appearance().tintColor = UIColor.red in AppDelegate.swift application(... didFinishLaunchingWithOptions ...) function then the selected item has a red (vs blue) tint.
What's happening?
I extend UITabBarController:
extension UITabBarController {
func updateTabBarItem(tab: Int, image: UIImage?) {
guard let tabItems = tabBar.items, tab < tabItems.count && tab >= 0
else { return }
let tabItem = tabItems[tab]
tabItem.image = image?.withRenderingMode(.alwaysOriginal)
tabItem.selectedImage = tabItem.image
}
}
This will help to access the tabBar.items without loading any view controllers (except the first view controller of the tab 0).
class MyViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.updateTabBarItem(tab: 1, image: UIImage(named: "selected-image")) // update the second tab's image here (just for example)
}
}
For example, if you want to change the tab 2 selected image, make a break point on viewDidLoad: on the second view controller, you will find the break point doesn't hit, that's why the tab item's image wouldn't be updated.
I am using storyboard to create a window controller with a NSToolbar.
Using the toolbar buttons I'm trying to switch the contentViewController of the window to other viewControllers.
#IBAction func switchViewControllers(sender: NSToolbarItem) {
if sender.label == "one" {
self.window?.contentViewController = self.storyboard?.instantiateControllerWithIdentifier("one") as? NSViewController
}
if sender.label == "two" {
self.window?.contentViewController = self.storyboard?.instantiateControllerWithIdentifier("two") as? NSViewController
}
}
Clicking on the toolbarItems switches the ViewControllers as expected but I am noticing some graphic glitches at the edges of the window when switching thew ViewControllers.
Am I missing something?
Startup
When switching views