I'm trying to create a Pinterest style Tab Bar. Is it possible to customise UITabBar in that way or do you have to write a separate view to sit on top of everything if so how would you do that? It's basically just moved up with rounded corners and a shadow. How would you adjust the width of the tab bar and move it up?
Image of Tab Bar
If you want to make "RoundedTabbar" than you can create one like this ->
import UIKit
class RoundedTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let layer = CAShapeLayer()
layer.path = UIBezierPath(roundedRect: CGRect(x: 30, y: tabBar.bounds.minY + 5, width: tabBar.bounds.width - 60, height: tabBar.bounds.height + 10), cornerRadius: (tabBar.frame.width/2)).cgPath
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOffset = CGSize(width: 5.0, height: 5.0)
layer.shadowRadius = 25.0
layer.shadowOpacity = 0.3
layer.borderWidth = 1.0
layer.opacity = 1.0
layer.isHidden = false
layer.masksToBounds = false
layer.fillColor = UIColor.white.cgColor
tabBar.layer.insertSublayer(layer, at: 0)
if let items = tabBar.items {
items.forEach { item in
item.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: -15, right: 0)
}
}
tabBar.itemWidth = 30.0
tabBar.itemPositioning = .centered
}
}
Don’t forget to type if you are using storyboard, the class name in the Identity inspector.
PS: I'm sharing Emmanuele Corporente's Medium article content as an answer please check the original article & give it some claps!
I created a floating tab bar library. It doesn't look exactly like Pinterest's but you could probably modify the drawing code to change the way it looks.
Here's the repo.
If you want to implement it from scratch, you'll have to create your own view controller subclass, as well as a UIView subclass for the tab bar, and manage the "page" switching yourself. UITabBarController doesn't offer enough customization for this.
Related
Can anyone point me how could I achieve such design in the UITabbar. I have tried adding the back-ground Image, but that does not look like the design. Here the curve is extended beyond the frame of UITabbar, not sure how to add this views on top of active tabbar.
Creating a custom TabBar from UITabBarController can be solved the problem. Instead of adding a direct image to the Tabbar, use an on the fly image using UIGraphicsBeginImageContext for selectedTabBackgroundImage.
Create the image.
Clip the top part round in the image
Here is the example of the code.
import UIKit
class CustomTabBarViewController: UITabBarController {
var topClipSize: CGFloat = 24.5 //Adjust based on the number of tabbar
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let singleTabWidth: CGFloat = self.tabBar.frame.size.width / CGFloat((self.tabBar.items?.count)!)
let singleTabSize = CGSize(width:singleTabWidth , height: self.tabBar.frame.size.height)
// Create the backgound image
let selectedTabBackgroundImage: UIImage = self.imageWithColor(color: .blue, size: singleTabSize)
// Clip the top
self.tabBar.selectionIndicatorImage = selectedTabBackgroundImage.roundTopImage(topClipSize: topClipSize)
}
func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
if size.height > 55 {
topClipSize = 30.0 // iPhone 8 tabbar height is 53 and iPnone X is 83 - We need more space on top.
}
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height + topClipSize)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor)
context!.fill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
extension UIImage {
func roundTopImage(topClipSize: CGFloat) -> UIImage {
let rect = CGRect(origin:CGPoint(x: 0, y: 0), size: self.size)
let rectBounds: CGRect = CGRect(x: rect.origin.x, y: rect.origin.y + (topClipSize * 2), width: rect.size.width, height: rect.size.height - (topClipSize * 2))
let ovalBounds: CGRect = CGRect(x: rect.origin.x - topClipSize, y: rect.origin.y, width: rect.size.width + (topClipSize * 2), height: rect.size.height)
UIGraphicsBeginImageContextWithOptions(self.size, false, 1)
let rectPath = UIBezierPath(rect: rectBounds)
let ovalPath = UIBezierPath(ovalIn: ovalBounds)
rectPath.append(ovalPath)
rectPath.addClip()
self.draw(in: rect)
return UIGraphicsGetImageFromCurrentImageContext()!
}
}
Here is the output:
It's not really possible to elegantly change the native UITabBar's appearance to that extent. Your options are to create a custom container view controller that acts like a UITabBarController, or just hide the default tab bar and implement your own view in that space.
Even though it's less elegant because you'd be just throwing a view on top of the default tab bar, I actually like that method because you retain the benefits of the native UITabBarController (calling self.tabBarController? from its view controllers, it already adjusts layout margins, etc).
To do this, in your subclass of UITabBarController hide the tabBar:
self.tabBar.isHidden = true
self.tabBar.alpha = 0
Then after implementing your custom view however you want, just set the frame of your custom view to self.tabBar.frame in viewDidLayoutSubviews.
For changing viewControllers, call this when the user taps one of your custom tabs:
self.selectedIndex = newIndex
I have a tab bar that I'm trying to customize it's look. So far, I changed it's background color and added corner radius. I'm trying to add a shadow to it, but I can't get it to work.
I tried few solutions, but nothing worked for me. There is not errors or anything, but it just won't work. This is what I have so far:
private func setupTabBarAppearance() {
let CORNER_RADIUS: CGFloat = 20
let tabBar = self.tabBar
tabBar.barTintColor = UIColor.appWhite
tabBar.unselectedItemTintColor = .lightGray
tabBar.tintColor = UIColor.appBlue
tabBar.layer.masksToBounds = true
tabBar.layer.cornerRadius = CORNER_RADIUS
tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
tabBar.layer.shouldRasterize = true
let shapeLayer = CAShapeLayer()
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: tabBar.frame.height), byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: CORNER_RADIUS, height: CORNER_RADIUS)).cgPath
shapeLayer.path = path
shapeLayer.shadowColor = UIColor.black.cgColor
shapeLayer.shadowOffset = CGSize(width: 2, height: 5)
shapeLayer.shadowOpacity = 1
shapeLayer.shadowRadius = 10
shapeLayer.shouldRasterize = true
shapeLayer.rasterizationScale = UIScreen.main.scale
tabBar.layer.rasterizationScale = UIScreen.main.scale
tabBar.layer.addSublayer(shapeLayer)
}
I have two problems with this that I don't find a solution to, which is:
I can't get the shapeLayer to be behind tabBar, it just covers it.
The shadow from shapeLayer is facing downwards so even if I would be able to get the shapeLayer to be behind the tab bar, the shadow still won't be displayed.
Let’s just tackle this part:
tabBar.layer.addSublayer(shapeLayer)
Obviously if you add a shape layer to the tab bar’s own layer, the shape layer must be in front. That is what it means to be a sublayer.
If you want to cast a shadow using a custom shape, you will need a completely separate view hidden behind your tab bar.
I'm trying to build an app in Xcode and implement buttons to the storyboard.
unfortunately the button frame is quite small in reference to the text inside. How to I change the frame into bigger?
As I am totally new to coding I have no idea - this is the code, but I don't know how to change the frame size and if this is even the right code.
extension UIButton {
func applyDesign() {
self.backgroundColor = UIColor.darkGray
self.layer.cornerRadius = 7
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowRadius = 5
self.setTitleColor(UIColor.white, for: .normal)
self.frame.size =
}
If you are creating buttons inside storyboard, you can simply use constraints for frames. If you want to do it manually like you mentioned then you can create a CGRect and assign it to frame, for example:
self.frame = CGRect(x: 100, y: 50, width: 170, height: 30)
Hi there,
I have a class CardButton: UIButton . In the draw method of this CardButton, I would like to add and center(vertically and horizontally) NSAttributed String, which is basically just one Emoji, inside of it. The result would look something like this:
However, NSAttributedString can be only aligned to center in horizontal dimension inside the container.
My idea for solution:
create a containerView inside of CardButton
center containerView both vertically and horizontally in it's container(which is CardButton)
add NSAttributedString inside the containerView and size containerView to fit the string's font.
So the result would look something like this:
My attempt for this to happen looks like this:
class CardButton: UIButton {
override func draw(){
//succesfully drawing the CardButton
let stringToDraw = attributedString(symbol, fontSize: symbolFontSize) //custom method to create attributed string
let containerView = UIView()
containerView.backgroundColor = //green
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
let centerXConstraint = containerView.centerXAnchor.constraint(equalTo: (self.centerXAnchor)).isActive = true
let centerYConstraint = containerView.centerYAnchor.constraint(equalTo: (self.centerYAnchor)).isActive = true
stringToDraw.draw(in: containerView.bounds)
containerView.sizeToFit()
}
}
Long story short, I failed terribly. I first tried to add containerView to cardButton, made the background green, gave it fixed width and height jut to make sure that it got properly added as a subview. It did. But once I try to active constraints on it, it totally disappears.
Any idea how to approach this?
There is always more than one way to achieve any particular UI design goal, but the procedure below is relatively simple and has been adapted to suit the requirements as presented and understood in your question.
The UIButton.setImage method allows an image to be assigned to a UIButton without the need for creating a container explicitly.
The UIGraphicsImageRenderer method allows an image to be made from various components including NSAttributedText, and a host of custom shapes.
The process utilising these two tools to provide the rudiments for your project will be to:
Render an image with the appropriate components & size
Assign the rendered image to the button
A class could be created for this functionality, but that has not been explored here.
Additionally, your question mentions that when applying constraints the content disappears. This effect can be observed when image dimensions are too large for the container, constraints are positioning the content out of view and possibly a raft of other conditions.
The following code produces the above image:
func drawRectangleWithEmoji() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { (ctx) in
// Create the outer square:
var rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 7.5, dy: 7.5)
var roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 50).cgPath
// MARK: .cgPath creates a CG representation of the path
ctx.cgContext.setFillColor(UIColor.white.cgColor)
ctx.cgContext.setStrokeColor(UIColor.blue.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Create the inner square:
rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 180, dy: 180)
roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 10).cgPath
ctx.cgContext.setFillColor(UIColor.green.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Add emoji:
var fontSize: CGFloat = 144
var attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize)//,
//.backgroundColor: UIColor.gray //uncomment to see emoji background bounds
]
var string = "❤️"
var attributedString = NSAttributedString(string: string, attributes: attrs)
let strWidth = attributedString.size().width
let strHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - strWidth / 2, y: 256 - strHeight / 2))
// Add NSAttributedString:
fontSize = 56
attrs = [
.font: UIFont.systemFont(ofSize: fontSize),
.foregroundColor: UIColor.brown
]
string = "NSAttributedString"
attributedString = NSAttributedString(string: string, attributes: attrs)
let textWidth = attributedString.size().width
let textHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - textWidth / 2, y: 384 - textHeight / 2))
}
return img
}
Activate the NSLayoutContraints and then the new image can be set for the button:
override func viewDidLoad() {
super.viewDidLoad()
view = UIView()
view.backgroundColor = .white
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
// buttonsView.backgroundColor = UIColor.gray
view.addSubview(buttonsView)
NSLayoutConstraint.activate([
buttonsView.widthAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.heightAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
let cardButton = UIButton(type: .custom)
cardButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
cardButton.setImage(drawRectangleWithEmoji(), for: .normal)
let frame = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth)
cardButton.frame = frame
buttonsView.addSubview(cardButton)
}
Your comments will be appreciated as would constructive review of the code provided.
I'm developing an app to display a binary tree.
Each node will be displayed as a subview programatically generated from the ViewController - I run the following from viewDidLayoutSubviews().
let theView = BinaryTreeView(frame: CGRect(x: 0, y: 50, width: width, height: 100))
// let theView = BinaryTreeView(s: "I'm testing")
theView.backgroundColor = .white
theView.addGestureRecognizer(UIPinchGestureRecognizer(target:theView, action:#selector(BinaryTreeView.changeScale(recognizer:))))
self.view.addSubview(theView)
theView.eyesOpen = false
let secondView = BinaryTreeView(frame: CGRect(x: width/2, y: 150, width: width/2, height: 100))
// let theView = BinaryTreeView(s: "I'm testing")
secondView.backgroundColor = .white
secondView.addGestureRecognizer(UIPinchGestureRecognizer(target:secondView, action:#selector(BinaryTreeView.changeScale(recognizer:))))
self.view.addSubview(secondView)
let thirdView = BinaryTreeView(frame: CGRect(x: (width/2)+width/4, y: 250, width: width/4, height: 100))
// let theView = BinaryTreeView(s: "I'm testing")
thirdView.backgroundColor = .white
thirdView.addGestureRecognizer(UIPinchGestureRecognizer(target:thirdView, action:#selector(BinaryTreeView.changeScale(recognizer:))))
self.view.addSubview(thirdView)
The issue is that on orientation change the views repeat each other (above there are three nodes, on orientation change 4 might display.
I looked through Stack and Within my subclassed UIView I added:
self.contentMode = UIViewContentMode.redraw
Within the programatic Subview but the same happens.
Don't worry - I'm going to generate my nodes in a loop later (I'm trying to understand how layout works). Incidentally I found the same happened using a UICollectionView so I seem to be doing something fundamentally wrong.
Change the "target:" to the controller (to self) for all three. Your controller will respond to gestures, not the views themselves. The target will be the same in all three cases.
secondView.addGestureRecognizer(UIPinchGestureRecognizer(target:secondView, action:#selector(BinaryTreeView.changeScale)))
self.view.addSubview(secondView)
becomes
secondView.addGestureRecognizer(UIPinchGestureRecognizer(target:self, action:#selector(BinaryTreeView.changeScale)))
self.view.addSubview(secondView)