How to add a custom border to the badge in the tab bar item? By default it shows a red dot in the top right corner of the tab bar item. I want to add a border to it. There is an option to change the color of the badge but I could not find anything to add a border to the badge.
I don't think there is an official way of adding a border to the badge, but I also wanted to do this and I ended up developing my own way. My solution is more like a hack, but it works on Xcode 10.1 and iOS 12.1 and will not get your app rejected, so maybe it's good enough for you.
The hardest part is to actually get a hold of the badge, this is the view hierarchy for the tab bar with 5 tabs:
You can see that in my project the last tab holds the badge. But actually it's the 4th tab (the one with index = 3) that has the badge, this is because I bring the 4th tab to top in my UITabBarController class so the badge is not clipped in case it displays bigger values. Here is how I get the badge view (this is in the UITabBar context, so self is UITabBar):
func badge(forIndex index: Int) -> UIView? {
let sorted = self.subviews.sorted { (view1, view2) -> Bool in // I need to sort it by the frame.origin.x because the order in the subviews array cannot be trusted, I got random order there during my tests
return view1.frame.origin.x < view2.frame.origin.x
}
let idx = index + 1 // the first view is actually a _UIBarBackground
guard let barItemView = sorted[safe: idx] else {
print("\(#file):\(#function):\(#line): Could not find a subview with index \(idx) in \(self)")
return nil
}
if barItemView.subviews.count == 3 { // this is a hack, but I could not find a better way to get the badge without using private interfaces
let view = barItemView.subviews.last
return view
}
return nil
}
This gives you the badge view, and with the hardest (and ugliest) part behind, we can now add the border (or change the position as I do in my project):
if let badgeView = badge(forIndex: index) {
// I found that it's easier to transform the layer here instead of the view, because it's instantaneous
let badgeViewLayer = badgeView.layer
badgeViewLayer.transform = CATransform3DMakeTranslation(BadgeTranslationX, BadgeTranslationY, 1) // you can change the position of the badge with this
guard let badgeViewImageView = badgeView.subviews.first else {
print("\(#file):\(#function):\(#line): No image view inside the badge view")
return
}
badgeViewImageView.clipsToBounds = true // this is important, otherwise you may get artifacts when working with rounded corners etc.
let layer = badgeViewImageView.layer
layer.borderColor = UIColor.white.cgColor
layer.borderWidth = 2
layer.cornerRadius = 8
}
And here is what you get with the above code:
Of course, the color can be anything:
Related
Whenever I use the pickerview to switch views from Auto Rent to Schedule Rent it works perfectly. It is when I switch from Schedule Rent to Auto Rent that this black bar appears. I have attached the hierarchy of my content view. I thought it had to do with previous constraints added, so I remove a StackView whenever one view is chosen. For example, if Auto Rent is chosen, then I remove the StackView where the Schedule View is in:
//Holds Temp Stackviews
var stackViewHolder1: UIView?
var stackViewHolder2: UIView?
override func viewDidLoad() {
super.viewDidLoad()
stackViewHolder1 = stackViewMain.arrangedSubviews[0]
stackViewHolder2 = stackViewMain.arrangedSubviews[1]
}
if txtRentType.text == "Auto Rent" {
let tempView = stackViewHolder1
let tempView1 = stackViewHolder2
tempView!.isHidden = true
stackViewMain.removeArrangedSubview(tempView!)
if(tempView1!.isHidden == true){
tempView1!.isHidden = false
stackViewMain.addArrangedSubview(tempView1!)
}
else{
let tempView = stackViewHolder1
let tempView1 = stackViewHolder2
tempView1!.isHidden = true
stackViewMain.removeArrangedSubview(tempView1!)
if(tempView!.isHidden == true){
tempView!.isHidden = false
stackViewMain.addArrangedSubview(tempView!)
}
}
I have tried deleting one view and toggling only one view has being hidden and that removes the black bar issue. There is no constraint with the stackViews and Content View.
EDIT:
The screen highlighted is the scrollView. The one after is the contentView. UIWindow goes black in the back.
My Title Bar at the top ends up in the middle somehow.
You can try to modify your stack distribution property
stack.distribution = .equalCentering
After you won't need to use this:
.removeArrangedSubview()
.addArrangedSubview()
When you hide some view, the other view take all space of your stack, you don't need to update your constraints. You can try it on interface builder to see how it works.
are you pinning your scrollview and the content view to the bottom with constraints?
If the content view is a stack view you can pin it to the bottom as well with layout constraints and play with the content distribution.
You don't need to use remove/Add arranged subviews.
when hiding a view in a stackView its automatically removed.
so i think you can just hide or show the stackViewMain.subviews[0] o stackViewMain.subviews[1]
i'm with objc maybe i do a mistake but it would be something like this :
if txtRentType.text == "Auto Rent" {
stackViewMain.arrangedSubviews[0].isHidden = true;
stackViewMain.arrangedSubviews[1].isHidden = false;
}else{
stackViewMain.arrangedSubviews[1].isHidden = true;
stackViewMain.arrangedSubviews[0].isHidden = false;
}
I need to put a search bar at the top of my tableview. I am making a network call and when the results are greater than 100 I want to search bar to appear and when they are less than 100 I don't want to search bar to appear. The tableview is on the right side of the VC and does not take up the whole view controller. I want the search bar to be at the top of the table view as a header.
I cannot use a search controller because in iOS 11, using a search controller makes the search bar pop to the top of the VC when it is active.
I tried to set the tableviewheader to nil to get it to disappear. But I can't get it back obviously because I made the header nil.
self.rightDetailFilterTableView.tableHeaderView = nil
self.rightDetailFilterTableView.sectionHeaderHeight = 0
I have put the search bar into the storyboard as seen in the image below. Is this the right way to add the search bar as a header?
What is the best way to get it to appear and disappear in the tableview? I have tried a bunch of different methods. They all either leave a blank header or do something else that causes problems. I also tried using the header delegate methods but that still did not work.
I am not using a tableview controller, I am using a normal VC. I am also not using a search bar controller because of issues it causes in iOS 11.
Here's what I've done in one of my recent project. First, laid out my views like so:
That is, the Search Bar was added to the parent view rather than the table view. This allows me to hide/show it as needed.
Next, I've defined two optional layout constraint, one ensuring that the tableview is aligned to the top of the safe area, priority 750; the other aligning the top of the search bar to the top of the safe area; priority lower than 750 to hide it below the nav bar or priority higher than 750 to reveal it and push the table view down.
In code, I created a #IBOutlet to the layout constraint for the search bar to the top of the safe area, and I change its priority as needed:
#IBAction
func toggleSearchBar(_ sender: Any?) {
if searchBarVisibleLayoutConstraint.priority.rawValue > 750.0 {
searchBarVisibleLayoutConstraint.priority = UILayoutPriority(rawValue: 1.0)
searchBar?.endEditing(true)
} else {
searchBarVisibleLayoutConstraint.priority = UILayoutPriority(rawValue: 999.0)
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
In my case, the navigation bar is opaque and the search bar is not visible behind it. Your case may be different so you may also want to either clip the parent view or alpha fade the search bar when it is not visible.
Good luck!
Please check :
Created IBOutlet for my SearchBar.
#IBOutlet weak var testbar: UISearchBar!
And in my viewDidLoad :
override func viewDidLoad() {
var contentOffset = tableView.contentOffset
let showSearchBar = (results.count > 100)
self.tableView.tableHeaderView?.isHidden = !(showSearchBar)
if showSearchBar {
contentOffset.y -= testbar.frame.size.height
} else {
contentOffset.y += testbar.frame.size.height
}
tableView.contentOffset = contentOffset
}
Here is my tableview storyboard
I'm creating an app that needs to show a tableview like below image
Similar colored circles are to be matched with a line.
Which view i can add the lines?
Or need to create a new view above tableview? But still my tableview needs to be scrolled.
How can i achieve this?
Update for Bounty
I want to implement the same with incliend lines between neighbouring circles. How to achieve the same?
Demonstration below:
create design like this
Based on your requirement just hide upper line and lower line of circle
You need to create collection view in tableview cell. In collection view you create one cell. Design the same user interface like your design. Show and hide the view with matching of rule. It will not affect tableview scrolling. and with this approach you can also provide scroll in collection view cell. i can provide you coded solution if you able to provide me more information. Thanks
You can use this Third Party LIb
You need to use a combination of collection view and a table view to give support for all devices.
1.Create one collection view cell with following layout
Hide upper and lower lines as per your need
Add collection view in table view cell and managed a number of cells in collection view depending upon the current device width and item's in between spacing.
You can create a vertical label without text, set the background color with black and place it behind the circle in view hierarchy and set a width of the label as per your requirement. Then you can hide unhide the label whenever you want.
P.S.: Make sure to hide your cell separator.
I have created a demo project. You can find it here. I tried to match your requirements. You can update the collection view settings to handle the hide and show of labels.
Let me know if you have any questions.
Hope this help.
Thanks!
To connect any circle with any other circle in the cell above / below, it will be easier and cleaner to create the connection lines dynamically rather than building them into the asset as before. This part is simple. The question now is where to add them.
You could have the connection lines between every two cells be contained in the top or bottom cell of each pair, since views can show content beyond their bounds.
There's a problem with this though, regardless of which cell contains the lines. For example, if the top cell contains them, then as soon as it is scrolled up off screen, the lines will disappear when didEndDisplayingCell is called, even though the bottom cell is still completely on screen. And then scrolling slightly such that cellForRow is called, the lines will suddenly appear again.
If you want to avoid that problem, then here is one approach:
One Approach
Give your table view and cells a clear background color, and have another table view underneath to display a new cell which will contain the connection lines.
So you now have a background TVC, with a back cell, and a foreground TVC with a fore cell. You add these TVC's as children in a parent view controller (of which you can set whatever background color you like), disable user interaction on the background TVC, and peg the background TVC's content offset to the foreground TVC's content offset in an observation block, so they will stay in sync when scrolling. I've done this before; it works well. Use the same row height, and give the background TVC a top inset of half the row height.
We can make the connection lines in the back cell hug the top and bottom edges of the cell. This way circles will be connected at their centre.
Perhaps define a method in your model that calculates what connections there are, and returns them, making that a model concern.
extension Array where Element == MyModel {
/**
A connection is a (Int, Int).
(0, 0) means the 0th circle in element i is connected to the 0th circle in element j
For each pair of elements i, j, there is an array of such connections, called a mesh.
Returns n - 1 meshes.
*/
func getMeshes() -> [[(Int, Int)]] {
// Your code here
}
}
Then in your parent VC, do something like this:
class Parent_VC: UIViewController {
var observation: NSKeyValueObservation!
var b: Background_TVC!
override func viewDidLoad() {
super.viewDidLoad()
let b = Background_TVC(model.getMeshes())
let f = Foreground_TVC(model)
for each in [b, f] {
self.addChild(each)
each.view.frame = self.view.bounds
self.view.addSubview(each.view)
each.didMove(toParent: self)
}
let insets = UIEdgeInsets(top: b.tableView.rowHeight / 2, left: 0, bottom: 0, right: 0)
b.tableView.isUserInteractionEnabled = false
b.tableView.contentInset = insets
self.b = b
self.observation = f.tableView.observe(\.contentOffset, options: [.new]) { (_, change) in
let y = change.newValue!.y
self.b.tableView.contentOffset.y = y // + or - half the row height
}
}
}
Then of course there's your drawing code. You could make it a method of your back cell class (a custom cell), which will take in a mesh data structure and then draw the lines that represent it. Something like this:
class Back_Cell: UITableViewCell {
/**
Returns an image with all the connection lines drawn for the given mesh.
*/
func createMeshImage(for mesh: [(Int, Int)]) -> UIImage {
let canvasSize = self.contentView.bounds.size
// Create a new canvas
UIGraphicsBeginImageContextWithOptions(canvasSize, false, 0)
// Grab that canvas
let canvas = UIGraphicsGetCurrentContext()!
let spacing: CGFloat = 10.0 // whatever the spacing between your circles is
// Draw the lines
for each in mesh {
canvas.move(to: CGPoint(x: CGFloat(each.0) * spacing, y: 0))
canvas.addLine(to: CGPoint(x: CGFloat(each.1) * spacing, y: self.contentView.bounds.height))
}
canvas.setStrokeColor(UIColor.black.cgColor)
canvas.setLineWidth(3)
canvas.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
You'd probably want to create a Mesh class and store the images in that model, to avoid redrawing.
I am using UIPageControl in conjunction with UIScrollView to display 5 images. I would like to be able to set the current page indicator dot color to a different color for each page (when it is selected). I have tried setting the currentPageIndicatorTintColor in the Value Changed action of the UIPageControl, but this shows the previous color briefly before changing to the new color.
Does anyone know if its possible to use different colors for each dot, in such a way that the display is seemless?
Note: this is not a duplicate of How can i change the color of pagination dots of UIPageControl? because that question is about changing all the dots to have the same color, whereas I would like to change the individual page dots to have different colors when their page is currently showing.
This is how you can do this:
Step 1: Subclass UIPageControl and add a property dotImages to hold images for all dots.
class MyPageControl : UIPageControl {
var dotImages : [UIImage?] = []
Step 2: Initialise your dotImages
required init(coder: NSCoder) {
super.init(coder: coder)!
let firstDotImage = UIImage.init(named: "dot1.png")
let secondDotImage = UIImage.init(named: "dot2.png")
let thirdDotImage = UIImage.init(named: "dot3.png")
let fourthDotImage = UIImage.init(named: "dot4.png")
let fifthDotImage = UIImage.init(named: "dot5.png")
dotImages = [firstDotImage, secondDotImage, thirdDotImage, fourthDotImage, fifthDotImage]
}
Step 3: Implement updateDots function to set your own image
func updateDots () {
for var index = 0; index < self.subviews.count; index++ {
let dot = self.subviews[index] as! UIImageView
dot.image = self.dotImages[index]
}
}
Step 4: Call updateDots function based on you app need. You may call it initially while view is loading or may be while page is changing.
currentPageIndicatorTintColor WILL work.
You might have to actually do a bit of work here.
func scrollViewDidScroll(scrollView: UIScrollView) {
//If the page is over 50% scrolled change the dot to the new value.
//If it is less, change it back if the user is holding the page.
}
What I am suggesting is listen to the scrollview, and pre-empt the page, rather than waiting for the fact to happen. My Psuedo-code is kept abstract, but if this is not enough I can modify the answer more specifically depending on your code if you provide examples.
You must therefore be a UIScrollViewDelegate
I'm took the Tabbar viewcontroller in this ,I added the 5 item and .I given the image insects is (24,0,0,6).
All button images are added in xib [under the Bar item -->image]Please help.
Thanks.
Adding to a similar answer here:
iOS Tab Bar icons keep getting larger
Not sure if this is an iOS7 bug but I've noticed that image insets need to be balanced.
You have specified insets for top and right but:
if you set a top inset, in order to balance it, you need to set the negative of it to the bottom inset
if you set a right inset, in order to balance it, you need to set the negative of it to the left inset
So, instead of having image insets like (24,0,0,6), use balanced image insets such as UIEdgeInsetsMake(24,-6,-24,6)
Doing so should protect your tabBarItem image from getting whacked on every tap.
If this doesn't suit your requirements, then redesign your tabBarItem image so you can have balance insets or... no insets at all.
Here's the workaround for a bug I've encountered with UITabBarController's UITabBar. If I tap a UITabBarItem once after it's selected, the icon shrinks. What I'd like to do is disable touches. UITabBarItem only has a setting for isEnabled, which grays it out if I set it to false...not what I was looking for.
I used a derivative of this answer to figure it out. With a UITabBarController with 3 tabs, printing tabBarController.subviews, I saw 3 UITabBarButtons and a UIBarBackground. The origin of UIBarBackground's frame was always (0, 0), putting it at the front of the sorted array, so I really don't need to know what the subview is, just "where it is" and whether it will always be there. The UIBarBackground is always going to be at the front of an array of tabBarController.subviews sorted by frame.minX, so I just need to remove it from the front.
Solution
Here's what the extension looks like:
extension UITabBarController {
var buttonViews: [UIView] {
var tabBarButtons = tabBar.subviews.sorted(by: {$0.frame.minX < $1.frame.minX})
tabBarButtons.removeFirst()
return tabBarButtons
}
}
I also created a struct in my Constants file, so I don't have to remember tab names:
struct TabBarItem {
static let firstTab = 0
static let secondTab = 1
static let thirdTab = 2
}
...and finally, where to use it:
In viewDidAppear (NOT viewDidLoad), add the following line to disable the UITabBarItem that you don't want to disable, but not gray out:
tabBarController?.buttonViews[TabBarItem.firstTab].isUserInteractionEnabled = false
In viewWillDisappear, re-enable the tab, as follows:
tabBarController?.buttonViews[TabBarItem.firstTab].isUserInteractionEnabled = true