Animate tab bar shape created using UIBezierPath() based on selected index - ios

I have created a tab Bar shape using UIBezierPath(). Im new to creating shapes so in the end I got the desired shape closer to what I wanted, not perfect but it works. It's a shape which has a wave like top, so on the left side there is a crest and second half has a trough. This is the code that I used to create the shape:
func createPath() -> CGPath {
let height: CGFloat = 40.0 // Height of the wave-like curve
let extraHeight: CGFloat = -20.0 // Additional height for top left and top right corners
let path = UIBezierPath()
let width = self.frame.width
// Creating a wave-like top edge for tab bar starting from left side
path.move(to: CGPoint(x: 0, y: extraHeight)) // Start at top left corner with extra height
path.addQuadCurve(to: CGPoint(x: width/2, y: extraHeight), controlPoint: CGPoint(x: width/4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width*3/4, y: extraHeight + height))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
Above im using -20 so that shape stays above bounds of tab bar and second wave's trough stays above the icons of tab bar. Here is the desired result:
This was fine until I was asked to animate the shape on pressing tab bar items. So if I press second item, the crest should be above second item and if fourth, then it should be above fourth item. So I created a function called updateShape(with selectedIndex: Int) and called it in didSelect method of my TabBarController. In that im passing index of the selected tab and based on that creating new path and removing old and replacing with new one. Here is how im doing it:
func updateShape(with selectedIndex: Int) {
let shapeLayer = CAShapeLayer()
let height: CGFloat = 40.0
let extraHeight: CGFloat = -20.0
let width = self.frame.width
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: extraHeight))
if selectedIndex == 0 {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 1 {
path.addQuadCurve(to: CGPoint(x: width / 2 + width / 4, y: extraHeight), controlPoint: CGPoint(x: width / 4 + width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 3 / 4 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 2 {
let xShift = width / 4
path.addQuadCurve(to: CGPoint(x: width / 2 + xShift, y: extraHeight), controlPoint: CGPoint(x: width / 8 + xShift, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 7 / 8 + xShift, y: extraHeight - height))
}
else {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight - height))
}
path.addLine(to: CGPoint(x: width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.secondarySystemBackground.cgColor
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
And calling it like this:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBar = tabBarController.tabBar as! CustomTabBar
guard let index = viewControllers?.firstIndex(of: viewController) else {
return
}
tabBar.updateShape(with: index)
}
This is working fine as you can see below but the problem is I learned creating shapes just now and creating that wave based on width of screen so the crest and trough are exactly half the width of frame so I was able to do it for FourthViewController too and got this:
But the problem arises for remaining 2 indices. Im not able to create same wave which looks like the crest is moving above second or third item instead I get something like this:
It doesn't look like other to waves showing the hump over third item. Also my code is strictly based on 4 items and was wondering if Im asked to add 1 more item so tab bar has 5 or 6 items, it would be trouble. Is there any way to update my function that creates new shapes based on index of tab bar or can anyone help me just create shapes for remaining two items? The shape should look same just the hump should exactly be over the selected item.

So you want something like this:
Here's how I did it. First, I made a WaveView that draws this curve:
Notice that the major peak exactly in the center. Here's the source code:
class WaveView: UIView {
var trough: CGFloat {
didSet { setNeedsDisplay() }
}
var color: UIColor {
didSet { setNeedsDisplay() }
}
init(trough: CGFloat, color: UIColor = .white) {
self.trough = trough
self.color = color
super.init(frame: .zero)
contentMode = .redraw
isOpaque = false
}
override func draw(_ rect: CGRect) {
guard let gc = UIGraphicsGetCurrentContext() else { return }
let bounds = self.bounds
let size = bounds.size
gc.translateBy(x: bounds.origin.x, y: bounds.origin.y)
gc.move(to: .init(x: 0, y: size.height))
gc.saveGState(); do {
// Transform the geometry so the bounding box of the curve
// is (-1, 0) to (+1, +1), with the y axis going up.
gc.translateBy(x: bounds.midX, y: trough)
gc.scaleBy(x: bounds.size.width * 0.5, y: -trough)
// Now draw the curve.
for x in stride(from: -1, through: 1, by: 2 / size.width) {
let y = (cos(2.5 * .pi * x) + 1) / 2 * (1 - x * x)
gc.addLine(to: .init(x: x, y: y))
}
}; gc.restoreGState()
// The geometry is restored.
gc.addLine(to: .init(x: size.width, y: size.height))
gc.closePath()
gc.setFillColor(UIColor.white.cgColor)
gc.fillPath()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Then, I made a subclass of UITabBarController named WaveTabBarController that creates a WaveView and puts it behind the tab bar. It makes the WaveView exactly twice the width of the tab bar, and sets the x coordinate of the WaveView's frame such that the peak is above the selected tab item. Here's the source code:
class WaveTabBarController: UITabBarController {
let waveView = WaveView(trough: 20)
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if superclass!.instancesRespond(to: #selector(UITabBarDelegate.tabBar(_:didSelect:))) {
super.tabBar(tabBar, didSelect: item)
}
view.setNeedsLayout()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let shouldAnimate: Bool
if waveView.superview != view {
view.insertSubview(waveView, at: 0)
shouldAnimate = false
} else {
shouldAnimate = true
}
let tabBar = self.tabBar
let w: CGFloat
if let selected = tabBar.selectedItem, let items = tabBar.items {
w = (CGFloat(items.firstIndex(of: selected) ?? 0) + 0.5) / CGFloat(items.count) - 1
} else {
w = -1
}
let trough = waveView.trough
let tabBarFrame = view.convert(tabBar.bounds, from: tabBar)
let waveFrame = CGRect(
x: tabBarFrame.origin.x + tabBarFrame.size.width * w,
y: tabBarFrame.origin.y - trough,
width: 2 * tabBarFrame.size.width,
height: tabBarFrame.size.height + trough
)
guard waveFrame != waveView.frame else {
return
}
if shouldAnimate {
// Don't animate during the layout pass.
DispatchQueue.main.async { [waveView] in
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
waveView.frame = waveFrame
}
}
} else {
waveView.frame = waveFrame
}
}
}

Related

Swift Tabbar with a custom shape in the middle

i want to make this shape in swift .
As you can see, the tabbar has a raised center button. However, this is not the only thing as there should be a real hole in the tabbar so that it is transparent there.
How can I create such a hole inside a tabbar? And then put a raised, round button in that hole?
I would gladly appreciate any help regarding my question.
i am trying but cannot achieve the above result.
import Foundation
import UIKit
#IBDesignable
class AppTabBar: UITabBar {
private var shapeLayer: CALayer?
override func draw(_ rect: CGRect) {
self.addShape()
}
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = #colorLiteral(red: 0.9782002568, green: 0.9782230258, blue: 0.9782107472, alpha: 1)
shapeLayer.lineWidth = 0.5
// The below 4 lines are for shadow above the bar. you can skip them if you do not want a shadow
shapeLayer.shadowOffset = CGSize(width:0, height:0)
shapeLayer.shadowRadius = 10
shapeLayer.shadowColor = UIColor.gray.cgColor
shapeLayer.shadowOpacity = 0.3
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
func createPath() -> CGPath {
let height2: CGFloat = self.frame.height
let height: CGFloat = 86.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
let startXpoint = centerWidth - height + 57
let endXpoint = (centerWidth + height - 45)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: startXpoint , y: 0))
// path.addCurve(to: CGPoint(x: centerWidth, y: height - 40),
// controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height - 40))
path.addCurve(to: CGPoint(x: centerWidth, y: height / 1.6),
controlPoint1: CGPoint(x: startXpoint - 5, y: height2 / 3), controlPoint2: CGPoint(x: (centerWidth - 10), y: height2 / 1.6))
path.addCurve(to: CGPoint(x: (centerWidth + height / 2.9 ), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height - 40), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
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
}
}
extension UITabBar {
override open func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFits = super.sizeThatFits(size)
sizeThatFits.height = 74
return sizeThatFits
}
}

Custom UITabBar irregular shape programmatically

I need to create custom tabBar irregular shape programmatically. I found a lot of decisions, but they all are connected to Interface Builder. The code is below. All the methods of customized tabBar don't call while debugging.
final class TabBar: UITabBarController {
var customTabBar = CustomizedTabBar()
override var tabBar: UITabBar {
return customTabBar
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .naviBarBlack
UITabBar.appearance().barTintColor = .naviBarBlack
UITabBar.appearance().clipsToBounds = false
tabBar.tintColor = .white
tabBar.itemPositioning = .centered
setupVCs()
}
func setupVCs() {
guard let homeUnselected = UIImage(named: "home-unselected"),
let homeSelected = UIImage(named: "home-selected"),
let likeUnselected = UIImage(named: "like-unselected"),
let likeSelected = UIImage(named:"like-selected") else {return}
self.viewControllers = [
createNavController(for: MainScreenViewController(),
image: homeUnselected,
selected: homeSelected),
createNavController(for: UIViewController(),
image: likeUnselected,
selected: likeSelected)
]
}
private func createNavController(for rootViewController: UIViewController,
image: UIImage,
selected: UIImage) -> UIViewController {
let navController = UINavigationController(rootViewController: rootViewController)
navController.tabBarItem.image = image
navController.tabBarItem.selectedImage = selected
return navController
}
}
class CustomizedTabBar: UITabBar {
private var shapeLayer: CALayer?
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 1.0
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
override func draw(_ rect: CGRect) {
self.addShape()
}
func createPath() -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let buttonRadius: CGFloat = 35
return abs(self.center.x - point.x) > buttonRadius || abs(point.y) > buttonRadius
}
func createPathCircle() -> CGPath {
let radius: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: (centerWidth - radius * 2), y: 0))
path.addArc(withCenter: CGPoint(x: centerWidth, y: 0), radius: radius, startAngle: CGFloat(180).degreesToRadians, endAngle: CGFloat(0).degreesToRadians, clockwise: false)
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
}
extension CGFloat {
var degreesToRadians: CGFloat { return self * .pi / 180 }
var radiansToDegrees: CGFloat { return self * 180 / .pi }
}
So this's what i want to see (from https://betterprogramming.pub/draw-a-custom-ios-tabbar-shape-27d298a7f4fa)
And what i get.
While the Apple docs for UITabBarController state:
You should never attempt to manipulate the UITabBar object itself stored in this property.
you can find many, many examples of custom tab bars out there.
For your specific approach, don't try overriding var tabBar:
Instead, if you have your TabBarController in Storyboard, assign the custom class of its TabBar to CustomizedTabBar.
Or, if you're instantiating the Controller from code, you could try this:
override func viewDidLoad() {
super.viewDidLoad()
let tabBar = { () -> CustomizedTabBar in
let tabBar = CustomizedTabBar()
tabBar.delegate = self
return tabBar
}()
self.setValue(tabBar, forKey: "tabBar")
// ... the rest of your viewDidLoad()
}
I'd recommend reading through several other examples though, and look for a common (reliable) approach.

Using a custom UITabbar inside UITabbarController programmatically

I'm trying to use my own UITabBar instance inside a UITabBarController. Using Storyboard I know that you can add your Custom Class to your TabbarController using the following:
How can I achieve the same thing programmatically?
Here's my custom class:
I've tried modifying and overriding the tabBar variable inside UITabBarController but it seems that it's a get only variable and can not be modified. Is there an easy solution to fix this issue?
Much appreciated!
class CustomTabBar: UITabBar {
private var shapeLayer: CALayer?
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 1.0
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
override func draw(_ rect: CGRect) {
self.addShape()
}
func createPath() -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let buttonRadius: CGFloat = 35
return abs(self.center.x - point.x) > buttonRadius || abs(point.y) > buttonRadius
}
func createPathCircle() -> CGPath {
let radius: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: (centerWidth - radius * 2), y: 0))
path.addArc(withCenter: CGPoint(x: centerWidth, y: 0), radius: radius, startAngle: CGFloat(180).degreesToRadians, endAngle: CGFloat(0).degreesToRadians, clockwise: false)
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
}
And here's my view controller:
final class TabbarViewController: UITabBarController {
// MARK: - Properties
private lazy var tabBarItemControllers: [UIViewController] = {
let editorController = ...
let settingsController = ...
return [editorController, settingsController]
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureTabbar()
setViewControllers(tabBarItemControllers, animated: true)
}
}

Creating triangle view fails when not added to x: 0 and y: 0

This code works when the frame's x and y are 0, but fails when using different x and y's:
class Triangle: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
let startX = self.center.x
let startY: CGFloat = 0
path.move(to: CGPoint(x: startX, y: startY))
path.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
path.addLine(to: CGPoint(x: 0, y: self.bounds.height))
path.close()
UIColor.green.setStroke()
path.stroke()
}
}
import UIKit
class ViewController: UIViewController {
#IBAction func animate(_ sender: UIButton) {
let triangleView = Triangle(frame: CGRect(x: 50, y: 50, width: 30, height: 30))
triangleView.backgroundColor = .clear
self.view.addSubview(triangleView)
}
}
This works:
let triangleView = Triangle(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
When failing it looks like this:
Well that is one ugly triangle. Why does it works with x: 0 and y:0 and fails when using different floats there? How can I fix this?
replace let startX = self.center.x with let startX = self. bounds.width / 2

Create slanted cut on image view swift

I have been looking around the internet and I can't find a good solution for making a slanted cut on an image view that works for swift.
Here is what I want
As you can see I would like to slant an image view as seen in the background. If anyone had some thoughts or solutions, that would be much appreciated.
Properties:
fileprivate var headerView: PostHeaderView!
fileprivate var headerMaskLayer: CAShapeLayer!
In viewDidLoad():
headerMaskLayer = CAShapeLayer()
headerMaskLayer.fillColor = UIColor.black.cgColor
headerView.layer.mask = headerMaskLayer
updateHeaderView()
Then use this function:
func updateHeaderView() {
let effectiveHeight = Storyboard.tableHeaderHeight - Storyboard.tableHeaderCutAway / 2
var headerRect = CGRect(x: 0, y: -effectiveHeight, width: tableView.bounds.width, height: Storyboard.tableHeaderHeight)
headerView.logoImageView.alpha = 0
if tableView.contentOffset.y < -effectiveHeight {
headerRect.origin.y = tableView.contentOffset.y
headerRect.size.height = -tableView.contentOffset.y + Storyboard.tableHeaderCutAway/2
let final: CGFloat = -100
let alpha = min((tableView.contentOffset.y + effectiveHeight) / final, 1)
headerView.logoImageView.alpha = alpha
}
headerView.frame = headerRect
// cut away
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: headerRect.width, y: 0))
path.addLine(to: CGPoint(x: headerRect.width, y: headerRect.height))
path.addLine(to: CGPoint(x: 0, y: headerRect.height - Storyboard.tableHeaderCutAway))
headerMaskLayer?.path = path.cgPath
}

Resources