how can i access UIBezierPath that is drawn in draw(_ rect: CGRect)? - swift

I'm drawing lines with UIBezierPath in draw(_ rect: CGRect) method previously i did tried that with CAShapeLayer but i wasn't able to select particular path and move it so i'm trying this, but after drawing i'm not able to access that BezierPath from view's subviews or it's sublayers and not directly from UIView. How can i access that bezierpath drawn in drawRect method so i can change it's position according to touches.
private var bezierPaths: [MyBezierPath] = [MyBezierPath]()
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.orange.set()
for path in bezierPaths {
path.lineWidth = 4
path.lineCapStyle = .round
path.lineJoinStyle = .round
path.stroke()
}
}
func drawingPath(_ path: [MyBezierPath]) {
bezierPaths = path
}
like creating CAShapeLayer and setting layer.path = BezierPath like this :
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineJoin = .round
shapeLayer.lineDashPattern = [10, 10]
shapeLayer.name = "ShapeLayer"
self.canvas.layer.addSublayer(shapeLayer)
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if canvas.layer.sublayers != nil && canvas.layer.sublayers?.last?.name == "ShapeLayer" {
guard let layer = canvas.layer.sublayers?.last as? CAShapeLayer else { return }
layer.path = path.cgPath
}
}
like this i can access layer.path but how can i do it for path drawn in draw(Rect:) method ?

Depending on what you're actually trying to do, overriding draw(_:) may not be the best approach.
For example, if you want to animate the drawn paths, it will be much easier if you are using CAShapeLayer as sublayers, or using subviews.
Also, shape layers are highly optimized ... if you write your own draw / animate functions you risk ending up with lesser-optimized code.
However, whichever approach you take, to "find the path" you want to use contains(_ point: CGPoint)
For example, if you have an array of UIBezierPath, with all paths relative to the top-left (0,0) of the view, on touch you could do:
// find a path that contains the touch point
if let touchedPath = bezierPaths.first(where: {$0.contains(point)}) {
// do something with that path
}
If the paths are not relative to the top-left of the view - for example, if the path is relative to the layer position, or is part of a subview - you'd need to convert the touch point.
Here's a quick example that looks like this - on load, and then after dragging a few shapes around:
We'll start with a simple "path" struct, which contains the bezier path and the color to use. We could add various other properties, such as line width, dash pattern, etc:
struct MyPath {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
}
then we'll use this UIView subclass that will handle the drawing, as well as touches began/moved/ended:
class BezDrawView: UIView {
var myPaths: [MyPath] = [] {
didSet {
setNeedsDisplay()
}
}
// used to track path to move
var activePath: MyPath?
var startPoint: CGPoint = .zero
override func draw(_ rect: CGRect) {
super.draw(rect)
if myPaths.count > 0 {
myPaths.forEach { p in
p.color.set()
p.path.stroke()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let point = t.location(in: self)
// find a path that contains the touch point
if let touchedPath = myPaths.first(where: {$0.path.contains(point)}) {
self.activePath = touchedPath
self.startPoint = point
return
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let ap = activePath, let t = touches.first else { return }
let point = t.location(in: self)
// move the path by the distance the touch moved
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
ap.path.apply(tr)
startPoint = point
// this triggers draw(_:)
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// done dragging
activePath = nil
}
}
and an example controller which defines some sample shapes (paths):
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let testBezDrawView = BezDrawView()
testBezDrawView.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
testBezDrawView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testBezDrawView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain bez draw view to all 4 sides with 20-points "padding"
testBezDrawView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testBezDrawView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testBezDrawView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testBezDrawView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// add some sample path shapes and colors
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow, .orange, .green,
]
let rects: [CGRect] = [
CGRect(x: 20, y: 20, width: 60, height: 60),
CGRect(x: 180, y: 20, width: 40, height: 60),
CGRect(x: 20, y: 120, width: 60, height: 100),
CGRect(x: 200, y: 140, width: 50, height: 90),
CGRect(x: 90, y: 220, width: 100, height: 60),
CGRect(x: 220, y: 260, width: 80, height: 160),
CGRect(x: 50, y: 360, width: 200, height: 100),
CGRect(x: 150, y: 480, width: 120, height: 80),
]
var somePaths: [MyPath] = []
var i: Int = 0
for (c, r) in zip(colors, rects) {
var b = UIBezierPath()
switch i % 4 {
case 1: // oval
b = UIBezierPath(ovalIn: r)
case 2: // triangle shape
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.maxY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.maxY))
b.close()
case 3: // diamond
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.maxY))
b.close()
default: // rect
b = UIBezierPath(rect: r)
}
b.lineWidth = 4
b.lineCapStyle = .round
b.lineJoinStyle = .round
b.setLineDash([5, 10], count: 2, phase: 0)
let p = MyPath(color: c, path: b)
somePaths.append(p)
i += 1
}
testBezDrawView.myPaths = somePaths
}
}
Here's a video of it in use (too big to convert to gif and embed here):
https://imgur.com/a/BmaHkKM
Edit in response to comment...
That needs very few changes to make it work with shape layers instead of draw(_:).
We can use the same Translation Transform to "move" the path, then update the .path property of that path's associated layer:
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
active.path.apply(tr)
activeLayer.path = activeParh.path.cgPath
I strongly, strongly recommend that you try to convert that sample code to shape layers on your own - it would be a good learning exercise.
But, if you run into trouble, here's a modified version...
First, we're going to use the. array index of the MyPath object to match with the sublayer index, so we need to make our struct Equatable:
struct MyPath: Equatable {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
}
Then some minor changes to BezDrawView -- which we'll name BezLayerView:
class BezLayerView: UIView {
var myPaths: [MyPath] = [] {
didSet {
// remove any existing layers
if let subs = layer.sublayers {
subs.forEach { lay in
lay.removeFromSuperlayer()
}
}
// create layers for paths
myPaths.forEach { p in
let lay = CAShapeLayer()
lay.lineWidth = 4
lay.lineCap = .round
lay.lineJoin = .round
lay.lineDashPattern = [5, 10]
lay.fillColor = UIColor.clear.cgColor
lay.strokeColor = p.color.cgColor
lay.path = p.path.cgPath
layer.addSublayer(lay)
}
}
}
// used to track path to move
var activeLayer: CAShapeLayer?
var activePath: MyPath?
var startPoint: CGPoint = .zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first, let subs = layer.sublayers else { return }
let point = t.location(in: self)
// find a path that contains the touch point
if let touchedPath = myPaths.first(where: {$0.path.contains(point)}) {
// find the layer associated with that path
if let idx = myPaths.firstIndex(of: touchedPath) {
if let lay = subs[idx] as? CAShapeLayer {
self.activePath = touchedPath
self.activeLayer = lay
self.startPoint = point
}
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let lay = activeLayer, let ap = activePath, let t = touches.first else { return }
let point = t.location(in: self)
// move the path by the distance the touch moved
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
ap.path.apply(tr)
lay.path = ap.path.cgPath
startPoint = point
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// done dragging
activeLayer = nil
activePath = nil
}
}
and an almost identical version of the controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let testBezLayerView = BezLayerView()
testBezLayerView.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
testBezLayerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testBezLayerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain bez draw view to all 4 sides with 20-points "padding"
testBezLayerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testBezLayerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testBezLayerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testBezLayerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// add some sample path shapes and colors
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow, .orange, .green,
]
let rects: [CGRect] = [
CGRect(x: 20, y: 20, width: 60, height: 60),
CGRect(x: 180, y: 20, width: 40, height: 60),
CGRect(x: 20, y: 120, width: 60, height: 100),
CGRect(x: 200, y: 140, width: 50, height: 90),
CGRect(x: 90, y: 220, width: 100, height: 60),
CGRect(x: 220, y: 260, width: 80, height: 160),
CGRect(x: 50, y: 360, width: 200, height: 100),
CGRect(x: 150, y: 480, width: 120, height: 80),
]
var somePaths: [MyPath] = []
var i: Int = 0
for (c, r) in zip(colors, rects) {
var b = UIBezierPath()
switch i % 4 {
case 1: // oval
b = UIBezierPath(ovalIn: r)
case 2: // triangle shape
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.maxY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.maxY))
b.close()
case 3: // diamond
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.maxY))
b.close()
default: // rect
b = UIBezierPath(rect: r)
}
let p = MyPath(color: c, path: b)
somePaths.append(p)
i += 1
}
testBezLayerView.myPaths = somePaths
}
}
The output and functionality should be indistinguishable from the BezDrawView implementation.

Related

SpriteKit progressive darkness in the Scene

Does anyone know how to bring progressive darkness on a SKScene using SpriteKit?
I tried using a SKLightNode, which works great to have "full" darkness in the scene and some light sources.
I also tried to have a Node in front of everything else where I adjust the alpha to get darker and darker. which works great if there is on light source
But the 2 solutions doesn't work together.
In my example, the blue bar on the buttom control the alpha of the "darkness" node (left = 0 so fully transparent, right = 1 so fully dark), and the white part on the buttom left is to swift on and off the light.
My goal would be to use the bar on the buttom to go from light on to light off, with a gradual transition.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let background = SKSpriteNode(color: .lightGray, size: view.frame.size) //imageNamed: "background")
background.zPosition = 1
background.position = CGPoint(x: view.frame.midX, y: view.frame.midY)
background.lightingBitMask = 0b0001
addChild(background)
let character = SKSpriteNode(color: .blue, size: CGSize(width: 100, height: 100))//imageNamed: "character")
character.zPosition = 2
character.position = CGPoint(x: view.frame.midX, y: view.frame.midY)
character.lightingBitMask = 0b0001
character.shadowCastBitMask = 0b0001
addChild(character)
let lightNode = SKLightNode()
lightNode.name = "SKLightNode"
lightNode.position = CGPoint(x: view.frame.midX, y: view.frame.midY)
lightNode.categoryBitMask = 0b0001
lightNode.lightColor = .white
//lightNode.ambientColor = .white
lightNode.isEnabled = false
addChild(lightNode)
// Control
let elementSize: CGFloat = 40
let lightToggleSize = CGSize(width: elementSize, height: elementSize)
let lightToggle = SKSpriteNode(color: .white, size: lightToggleSize)
lightToggle.name = "LightToggle"
lightToggle.zPosition = 10000
lightToggle.position = CGPoint(x: view.frame.width - (elementSize / 2), y: elementSize / 2)
addChild(lightToggle)
let sideBarSize = CGSize(width: view.frame.width - elementSize, height: elementSize)
let sideBar = SKSpriteNode(color: .blue, size: sideBarSize)
sideBar.name = "SideBar"
sideBar.position = CGPoint(x:(view.frame.width - elementSize) / 2, y: elementSize / 2)
sideBar.zPosition = lightToggle.zPosition
addChild(sideBar)
let darknessNodeSize = CGSize(width: view.frame.width , height: view.frame.height)
let darknessNode = SKSpriteNode(color: .black, size: darknessNodeSize)
darknessNode.name = "DarknessNode"
darknessNode.position = CGPoint(x: view.frame.width / 2, y: view.frame.height / 2)
darknessNode.alpha = 0
darknessNode.zPosition = lightToggle.zPosition - 1
addChild(darknessNode)
}
func handleTouches(_ point: CGPoint) {
let darknessNode = childNode(withName: "DarknessNode") as! SKSpriteNode
let lightNode = childNode(withName: "SKLightNode") as! SKLightNode
let sideBar = childNode(withName: "SideBar")!
let lightToggle = childNode(withName: "LightToggle") as! SKSpriteNode
if lightToggle.contains(point) {
lightNode.isEnabled = !lightNode.isEnabled
lightNode.position = CGPoint(x: lightNode.position.x + 1, y: lightNode.position.y)
} else if sideBar.contains(point) {
darknessNode.alpha = point.x / sideBar.frame.width
} else {
lightNode.position = point
}
}
override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {}
override func touchesMoved(_ touches: Set<UITouch>,
with event: UIEvent?) {
handleTouches(touches.first?.location(in: self))
}
override func touchesEnded(_ touches: Set<UITouch>,
with event: UIEvent?) {
handleTouches(touches.first?.location(in: self))
}
}
If anyone is interested by this topic, here is the solution I found: change the alpha of the source.
It works well on a Playground project but on my real one the local light is having a weird flat shape (I posted a question here
This is what it looks like.
And the solution is bellow.
There is a green background and blue box on the scene.
There are 2 white boxes that are switches for the Background light (that simulate the sunlight, which is off screen) and the "local" light (that simulate a light source)
The white bar is to control the alpha of the background light (on the left the alpha is 0 and there is no light and the right the alpha is 1 and the light is full on). In the same time as I change the alpha of the background light, I also change the alpha of the local light for better look.
import Foundation
import SpriteKit
class GameScene: SKScene {
var background: SKSpriteNode!
var object: SKSpriteNode!
var backgroundLight: SKLightNode!
var localLight: SKLightNode!
var backgroundLightSwitch: SKSpriteNode!
var backgroundLightBar: SKSpriteNode!
var localLightSwitch: SKSpriteNode!
var isBackgroundLightOn = false
var isLocalLightOn = false
var selectedElement: SKSpriteNode? = nil
class func newGameScene() -> GameScene {
let scene = GameScene()
scene.scaleMode = .resizeFill
return scene
}
override func didMove(to view: SKView) {
super.didMove(to: view)
let objectSize = CGSize(width: 100, height: 100)
background = createSpriteNode(color: .green, x: view.frame.width / 2, y: view.frame.height / 2, width: view.frame.width, height: view.frame.height, lightMask: 1)
object = createSpriteNode(color: .blue, x: background.position.x, y: background.position.y, width: objectSize.width, height: objectSize.height, lightMask: 1)
backgroundLightSwitch = createSpriteNode(color: .white, x: view.frame.width / 4, y: view.frame.height / 4, width: objectSize.width / 2, height: objectSize.height / 2, lightMask: 2)
backgroundLightBar = createSpriteNode(color: .white, x:view.frame.width * 2 / 3, y: backgroundLightSwitch.position.y, width: view.frame.width / 2, height: objectSize.height / 2, lightMask: 2)
localLightSwitch = createSpriteNode(color: .white, x: backgroundLightSwitch.position.x, y: view.frame.height / 8, width: objectSize.width / 2, height: objectSize.height / 2, lightMask: 2)
let color = NSColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5)
backgroundLight = createLightNode(color: nil, ambiantColor: color, x: -view.frame.width * 2, y: -view.frame.height * 2)
localLight = createLightNode(color: color, ambiantColor: nil, x:view.frame.width * 2 / 3, y: view.frame.height * 2 / 3)
}
func createSpriteNode(color: NSColor, x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, lightMask: UInt32) -> SKSpriteNode {
let nodePosition = CGPoint(x: x, y: y)
let nodeSize = CGSize(width: width, height: height)
let node = SKSpriteNode(color: color, size: nodeSize)
node.zPosition = 1
node.position = nodePosition
node.lightingBitMask = lightMask
addChild(node)
return node
}
func createLightNode(color: NSColor?, ambiantColor: NSColor? , x: CGFloat, y: CGFloat) -> SKLightNode {
let light = SKLightNode()
light.falloff = 1.5
light.position = CGPoint(x:x, y: y)
light.isEnabled = false
light.categoryBitMask = 1
if color != nil {
light.lightColor = color!
}
if ambiantColor != nil {
light.ambientColor = ambiantColor!
}
addChild(light)
return light
}
func getSelectElement(location: CGPoint) -> SKSpriteNode? {
let elements: [SKSpriteNode] = [backgroundLightSwitch, localLightSwitch, backgroundLightBar]
for element in elements {
if element.contains(location) {
return element
}
}
return nil
}
func switchLight(_ light: SKLightNode, selected: Bool) -> Bool {
light.isEnabled = selected
light.position.x += selected ? 1 : -1
return selected
}
func getAlpha(node: SKSpriteNode, location: CGPoint) -> CGFloat {
if location.x < node.frame.minX {
return 0
} else if location.x > node.frame.maxX {
return 1
} else {
return (location.x - node.frame.minX) / node.frame.width
}
}
func changeAlpha(_ alpha: CGFloat) {
backgroundLight.ambientColor = NSColor(red: 1.0, green: 1.0, blue: 1.0, alpha: alpha)
localLight.lightColor = NSColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1 - alpha)
backgroundLight.position.x -= 1
}
override func mouseDown(with event: NSEvent) {
let location = event.location(in: self)
selectedElement = getSelectElement(location: location)
}
override func mouseDragged(with event: NSEvent) {
if selectedElement == backgroundLightBar {
let location = event.location(in: self)
let newAlpha = getAlpha(node: backgroundLightBar, location: location)
changeAlpha(newAlpha)
}
}
override func mouseUp(with event: NSEvent) {
let location = event.location(in: self)
let newSelectedElement = getSelectElement(location: location)
if selectedElement == newSelectedElement {
if selectedElement == backgroundLightSwitch {
isBackgroundLightOn = switchLight(backgroundLight, selected: !isBackgroundLightOn)
} else if selectedElement == localLightSwitch {
isLocalLightOn = switchLight(localLight, selected: !isLocalLightOn)
}
}
}
}

curve in tabbar view and bottom popup not working?

Hello all, I tried to add arc for UIBezierPath I could not able to get the exact curve,
here is my code here I have added the bezier path for the added curve from the center position.
#IBDesignable
class MyTabBar: 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
//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
}
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
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), 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
}
}
this is tab bar controller added plus button in center view center, and the when tap the plus button to add the curve based popup should show, I don't know how to add curve based popup.
class TabbarViewController: UITabBarController,UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.navigationController?.navigationBar.isHidden = true
setupMiddleButton()
}
// TabBarButton – Setup Middle Button
func setupMiddleButton() {
let middleBtn = UIButton(frame: CGRect(x: (self.view.bounds.width / 2)-25, y: -20, width: 50, height: 50))
middleBtn.setImage(UIImage(named: "PlusBtn"), for: .normal)
self.tabBar.addSubview(middleBtn)
middleBtn.addTarget(self, action: #selector(self.menuButtonAction), for: .touchUpInside)
self.view.layoutIfNeeded()
}
// Menu Button Touch Action
#objc func menuButtonAction(sender: UIButton) {
//show the popUp
}
}
Please share me the findings & share me your refreance
Thanks
New edit:
I created a general-purpose method that will generate polygons with a mixture of sharp and rounded corners of different radii. I used it to create a project with a look rather like what you are after. You can download it from Github:
https://github.com/DuncanMC/CustomTabBarController.git
Here's what it looks like:
Note that the area of the tab's view controller that extends into the tab bar controller does not get taps. If you try to tap there, it triggers the tab bar controller. You will have to do some more tinkering to get that part to work.
Ultimately you may have to create a custom UITabBar (or UITabBar-like component) and possibly a custom parent view controller that acts like a UITabBar, in order to get what you want.
The method that creates polygon paths is called buildPolygonPathFrom(points:defaultCornerRadius:)
It takes an array of PolygonPoint structs. Those are defined like this:
/// A struct describing a single vertex in a polygon. Used in building polygon paths with a mixture of rounded an sharp-edged vertexes.
public struct PolygonPoint {
let point: CGPoint
let isRounded: Bool
let customCornerRadius: CGFloat?
init(point: CGPoint, isRounded: Bool, customCornerRadius: CGFloat? = nil) {
self.point = point
self.isRounded = isRounded
self.customCornerRadius = customCornerRadius
}
init(previousPoint: PolygonPoint, isRounded: Bool) {
self.init(point: previousPoint.point, isRounded: isRounded, customCornerRadius: previousPoint.customCornerRadius)
}
}
The code to build the path for the custom tab bar looks like this:
func tabBarMaskPath() -> CGPath? {
let width = bounds.width
let height = bounds.height
guard width > 0 && height > 0 else { return nil }
let dentRadius: CGFloat = 35
let cornerRadius: CGFloat = 20
let topFlatPartWidth = (width - dentRadius * 2.0) / 2
let polygonPoints = [
PolygonPoint(point: CGPoint(x: 0, y: 0), // Point 0
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: 0, y: height), // Point 1
isRounded: false),
PolygonPoint(point: CGPoint(x: width, y: height), // Point 2
isRounded: false),
PolygonPoint(point: CGPoint(x: width, y: 0), // Point 3
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth + dentRadius * 2, y: 0), // Point 4
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth + dentRadius * 2, y: dentRadius + cornerRadius), // Point 5
isRounded: true,
customCornerRadius: dentRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth , y: dentRadius + cornerRadius), // Point 6
isRounded: true,
customCornerRadius: dentRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth , y: 0), // Point 7
isRounded: true,
customCornerRadius: cornerRadius),
]
return buildPolygonPathFrom(points: polygonPoints, defaultCornerRadius: 0)
}
Previous answer:
I just tried it, and it is possible to subclass UITabBar. I created a subclass of UITabBar where I use a mask layer to cut a circular "notch" out of the top of the tab bar. The code is below. It looks like the screenshot below. It isn't quite what you're after, but it's a start:
(The background color for the "Page 1" view controller is set to light gray, and you can see that color showing through in the "notch" I cut out of the tab bar.)
//
// CustomTabBar.swift
// TabBarController
//
// Created by Duncan Champney on 3/31/21.
//
import UIKit
class CustomTabBar: UITabBar {
var maskLayer = CAShapeLayer()
override var frame: CGRect {
didSet {
configureMaskLayer()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configureMaskLayer()
self.layer.mask = maskLayer
self.layer.borderWidth = 0
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configureMaskLayer()
self.layer.mask = maskLayer
}
func configureMaskLayer() {
let rect = layer.bounds
maskLayer.frame = rect
let circleBoxSize = rect.size.height * 1.25
maskLayer.fillRule = .evenOdd
let path = UIBezierPath(rect: rect)
let circleRect = CGRect(x: rect.size.width/2 - circleBoxSize / 2,
y: -circleBoxSize/2,
width: circleBoxSize,
height: circleBoxSize)
let circle = UIBezierPath.init(ovalIn: circleRect)
path.append(circle)
maskLayer.path = path.cgPath
maskLayer.fillColor = UIColor.white.cgColor // Any opaque color works and has no effect
}
}
Edit:
To draw your popover view you'll need to create a filled path that shape. You'll have to construct a custom shape like that with a combination of lines and arcs. I suggest using a CGMutablePath and the method addArc(tangent1End:tangent2End:radius:transform:) since that enables you to provide endpoints rather than angles.
Edit #2:
Another part of the puzzle:
Here is a custom UIView subclass that masks itself in the shape you're after
//
// ShapeWithTabView.swift
// ShapeWithTab
//
// Created by Duncan Champney on 4/1/21.
//
import UIKit
class ShapeWithTabView: UIView {
var cornerRadius: CGFloat = 20
var tabRadius: CGFloat = 60
var tabExtent: CGFloat = 0
var shapeLayer = CAShapeLayer()
var maskLayer = CAShapeLayer()
func buildShapeLayerPath() -> CGPath {
let boxWidth = min(bounds.size.width - 40, 686)
let boxHeight = min(bounds.size.height - 40 - tabRadius * 2 - tabExtent, 832)
// These are the corners of the view's primary rectangle
let point1 = CGPoint(x: 0, y: boxHeight)
let point2 = CGPoint(x: 0, y: 0)
let point3 = CGPoint(x: boxWidth, y: 0)
let point4 = CGPoint(x: boxWidth, y: boxHeight)
// These are the corners of the "tab" that extends outside the view's normal bounds.
let tabPoint1 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight)
let tabPoint2 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight + tabExtent + tabRadius * 2 )
let tabPoint3 = CGPoint(x: boxWidth / 2 - tabRadius, y: boxHeight + tabExtent + tabRadius * 2)
let tabPoint4 = CGPoint(x: boxWidth / 2 - tabRadius , y: boxHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: boxHeight - cornerRadius))
path.addArc(tangent1End: point2,
tangent2End: point3,
radius: cornerRadius)
path.addArc(tangent1End: point3,
tangent2End: point4,
radius: cornerRadius)
path.addArc(tangent1End: point4,
tangent2End: point1,
radius: cornerRadius)
//
path.addArc(tangent1End: tabPoint1,
tangent2End: tabPoint2,
radius: tabRadius)
path.addArc(tangent1End: tabPoint2,
tangent2End: tabPoint3,
radius: tabRadius)
path.addArc(tangent1End: tabPoint3,
tangent2End: tabPoint4,
radius: tabRadius)
path.addArc(tangent1End: tabPoint4,
tangent2End: point1,
radius: tabRadius)
path.addArc(tangent1End: point1,
tangent2End: point2,
radius: cornerRadius)
return path
}
func doInitSetup() {
self.layer.addSublayer(shapeLayer)
self.layer.mask = maskLayer
backgroundColor = .lightGray
//Configure a shape layer to draw an outline
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 2
//Configure a mask layer to mask the view to our custom shape
maskLayer.fillColor = UIColor.white.cgColor
maskLayer.strokeColor = UIColor.white.cgColor
maskLayer.lineWidth = 2
}
override init(frame: CGRect) {
super.init(frame: frame)
self.doInitSetup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.doInitSetup()
}
public func updateShapeLayerPath() {
let path = buildShapeLayerPath()
shapeLayer.path = path
maskLayer.path = path
}
override var frame: CGRect {
didSet {
print("New frame = \(frame)")
shapeLayer.frame = layer.bounds
}
}
}
Combined with the modified tab bar from above, it looks like the image below. The final task is to get the custom view sized and positioned correctly, and have it land on top of the tab bar.

Custom Tab Bar, with central button which will be hide by pressed index

How I can create Custom tab bar with 4 items and one FAB in center BUT Fab button show only when i press on index 3, for index 0 .. 3 fab.isHidden = true. I am don't need animation that button if shift, but in fill be plus), only show / hide. For creating custom tab bat I am use this guide https://medium.com/better-programming/draw-a-custom-ios-tabbar-shape-27d298a7f4fa, I am need create tab bar like this , I am try maenter image description hereny other way but can't solved problem.
Custom tab bar which needed
I am try this way
import UIKit
#IBDesignable
class CustomizedTabBar: UITabBar {
private var shapeLayer: CALayer?
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.red.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 - 25), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
//
//// // second curve up
// path.addCurve(to: CGPoint(x: (centerWidth + height * 1.0), y: 0),
// controlPoint1: CGPoint(x: centerWidth + 25, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
// cirle inside tab bar
path.addArc(withCenter: CGPoint(x: centerWidth, y: 0), radius: height, startAngle: CGFloat(180).degreesToRadians, endAngle: CGFloat(0).degreesToRadians, clockwise: false)
// 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 }
}
And second way
import UIKit
class MainTabBar: UITabBar {
var checkState: Bool = true
public var middleButton = UIButton()
override func awakeFromNib() {
super.awakeFromNib()
guard let tabItems = items else { return }
tabItems[1].titlePositionAdjustment = UIOffset(horizontal: -10, vertical: 0)
tabItems[2].titlePositionAdjustment = UIOffset(horizontal: 10, vertical: 0)
setupMiddleButton()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isHidden {
return super.hitTest(point, with: event)
}
let from = point
let to = middleButton.center
return sqrt((from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y)) <= 39 ? middleButton : super.hitTest(point, with: event)
}
override func layoutSubviews() {
super.layoutSubviews()
middleButton.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: 5)
}
func setupMiddleButton() {
middleButton.frame.size = CGSize(width: 70, height: 70)
middleButton.layer.cornerRadius = 35
middleButton.layer.masksToBounds = true
middleButton.layer.borderWidth = 8
middleButton.layer.borderColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
middleButton.backgroundColor = #colorLiteral(red: 0.2447069331, green: 0.850134835, blue: 0.1531122658, alpha: 1)
middleButton.setImage(UIImage(named: "plus.png"), for: .normal)
middleButton.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: 0)
middleButton.addTarget(self, action: #selector(test), for: .touchUpInside)
addSubview(middleButton)
}
#objc func test() {
print("my name is jeff")
}
}
and I am try this way from article
https://equaleyes.com/blog/2017/09/04/the-common-raised-center-button-problems-in-tabbar/
but it's not working for me, also I am read too many info from WWW and don't get answer(
I am was created this tab bar, ours need few steps.
Create ViewController and Embed in "TabBarController", then need create TWO class first for "UITabBar" this class contain shape and what you want with "UITabBar", second class for "UITabBarController" for switch between ViewControllers inside we can add animation.... It's need because, my TabBar have 4 tabs and only on LAST tabs I am have Central FAB button with animation, and I am should animate position of my 2 and 3 ui tab bar element when button is appear.
Class for "UITabBar"
import UIKit
#IBDesignable
class CustomizedTabBar: UITabBar {
// MARK:- Variables -
#objc public var centerButtonActionHandler: ()-> () = {}
#IBInspectable public var centerButton: UIButton?
#IBInspectable public var centerButtonColor: UIColor?
#IBInspectable public var centerButtonHeight: CGFloat = 50.0
#IBInspectable public var padding: CGFloat = 5.0
#IBInspectable public var buttonImage: UIImage?
#IBInspectable public var buttonTitle: String?
#IBInspectable public var tabbarColor: UIColor = UIColor.lightGray
#IBInspectable public var unselectedItemColor: UIColor = .init(red: 0.58, green: 0.61, blue: 0.66, alpha: 1.0)
#IBInspectable public var selectedItemColor: UIColor = UIColor.black
public var arc: Bool = true {
didSet {
self.setNeedsDisplay()
}
}
private var shapeLayer: CALayer?
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.fillColor = #colorLiteral(red: 0.96, green: 0.96, blue: 0.96, alpha: 1)
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
self.tintColor = centerButtonColor
self.unselectedItemTintColor = unselectedItemColor
self.tintColor = selectedItemColor
self.setupMiddleButton()
}
override func draw(_ rect: CGRect) {
self.addShape()
}
override public 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
}
func createPath() -> CGPath {
let padding: CGFloat = 5.0
let centerButtonHeight: CGFloat = 53.0
let f = CGFloat(centerButtonHeight / 2.0) + padding
let h = frame.height
let w = frame.width
let halfW = frame.width/2.0
let r = CGFloat(18)
let path = UIBezierPath()
path.move(to: .zero)
if (!arc) {
path.addLine(to: CGPoint(x: halfW-f-(r/2.0), y: 0))
path.addQuadCurve(to: CGPoint(x: halfW-f, y: (r/2.0)), controlPoint: CGPoint(x: halfW-f, y: 0))
path.addArc(withCenter: CGPoint(x: halfW, y: (r/2.0)), radius: f, startAngle: .pi, endAngle: 0, clockwise: false)
path.addQuadCurve(to: CGPoint(x: halfW+f+(r/2.0), y: 0), controlPoint: CGPoint(x: halfW+f, y: 0))
}
path.addLine(to: CGPoint(x: w, y: 0))
path.addLine(to: CGPoint(x: w, y: h))
path.addLine(to: CGPoint(x: 0.0, y: h))
path.close()
return path.cgPath
}
private func setupMiddleButton() {
centerButton = UIButton(frame: CGRect(x: (self.bounds.width / 2)-(centerButtonHeight/2), y: -16, width: centerButtonHeight, height: centerButtonHeight))
centerButton!.setNeedsDisplay()
centerButton!.layer.cornerRadius = centerButton!.frame.size.width / 2.0
centerButton!.setTitle(buttonTitle, for: .normal)
centerButton!.setImage(UIImage(named: "plus"), for: .normal)
centerButton!.backgroundColor = .init(red: 0.07, green: 0.83, blue: 0.05, alpha: 1.0)
centerButton!.tintColor = UIColor.white
self.centerButton!.isHidden = true
if (!self.arc) {
DispatchQueue.main.async {
UIView.transition(with: self.centerButton!, duration: 1,
options: .transitionCrossDissolve,
animations: {
self.centerButton!.isHidden = false
})
}
}
//add to the tabbar and add click event
self.addSubview(centerButton!)
centerButton!.addTarget(self, action: #selector(self.centerButtonAction), for: .touchUpInside)
}
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
}
// Menu Button Touch Action
#objc func centerButtonAction(sender: UIButton) {
self.centerButtonActionHandler()
}
}
extension CGFloat {
var degreesToRadians: CGFloat { return self * .pi / 180 }
var radiansToDegrees: CGFloat { return self * 180 / .pi }
}
And class for UITabBarController
import UIKit
class MyTabBarController: UITabBarController {
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let myTabBar = tabBar as! CustomizedTabBar
if (myTabBar.items?[3] == item) {
myTabBar.arc = false
} else {
myTabBar.arc = true
}
}
}

Create custom Line graph with shadow using CAShapeLayer swift

I have modified Line graph of Minh Nguyen to some extend to show two lines one for systolic and othere for diastolic.
The first image show the how the graph should look like and second image is what I have achieved.
struct PointEntry {
let systolic: Int
let diastolic: Int
let label: String
}
extension PointEntry: Comparable {
static func <(lhs: PointEntry, rhs: PointEntry) -> Bool {
return lhs.systolic < rhs.systolic || lhs.systolic < rhs.systolic
}
static func ==(lhs: PointEntry, rhs: PointEntry) -> Bool {
return lhs.systolic == rhs.systolic && lhs.diastolic == rhs.diastolic
}
}
class LineChart: UIView {
/// gap between each point
let lineGap: CGFloat = 30.0
/// preseved space at top of the chart
let topSpace: CGFloat = 20.0
/// preserved space at bottom of the chart to show labels along the Y axis
let bottomSpace: CGFloat = 40.0
/// The top most horizontal line in the chart will be 10% higher than the highest value in the chart
let topHorizontalLine: CGFloat = 110.0 / 100.0
/// Dot inner Radius
var innerRadius: CGFloat = 8
/// Dot outer Radius
var outerRadius: CGFloat = 12
var dataEntries: [PointEntry]? {
didSet {
self.setNeedsLayout()
}
}
/// Contains the main line which represents the data
private let dataLayer: CALayer = CALayer()
/// Contains dataLayer and gradientLayer
private let mainLayer: CALayer = CALayer()
/// Contains mainLayer and label for each data entry
private let scrollView: UIScrollView = {
let view = UIScrollView()
view.showsVerticalScrollIndicator = false
view.showsHorizontalScrollIndicator = false
return view
}()
/// Contains horizontal lines
private let gridLayer: CALayer = CALayer()
/// An array of CGPoint on dataLayer coordinate system that the main line will go through. These points will be calculated from dataEntries array
private var systolicDataPoint: [CGPoint]?
private var daistolicDataPoint: [CGPoint]?
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
convenience init() {
self.init(frame: CGRect.zero)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
mainLayer.addSublayer(dataLayer)
mainLayer.addSublayer(gridLayer)
scrollView.layer.addSublayer(mainLayer)
self.addSubview(scrollView)
}
override func layoutSubviews() {
scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
if let dataEntries = dataEntries {
scrollView.contentSize = CGSize(width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height)
mainLayer.frame = CGRect(x: 0, y: 0, width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height)
dataLayer.frame = CGRect(x: 0, y: topSpace, width: mainLayer.frame.width, height: mainLayer.frame.height - topSpace - bottomSpace)
systolicGradientLayer.frame = dataLayer.frame
diastolicGradientLayer.frame = dataLayer.frame
systolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: true)
daistolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: false)
gridLayer.frame = CGRect(x: 0, y: topSpace, width: CGFloat(dataEntries.count) * lineGap + 30, height: mainLayer.frame.height - topSpace - bottomSpace)
clean()
drawHorizontalLines()
drawVerticleLine()
drawChart(for: systolicDataPoint, color: .blue)
drawChart(for: daistolicDataPoint, color: .green)
drawLables()
}
}
/// Convert an array of PointEntry to an array of CGPoint on dataLayer coordinate system
/// - Parameter entries: Arrays of PointEntry
private func convertDataEntriesToPoints(entries: [PointEntry], isSystolic: Bool) -> [CGPoint] {
var result: [CGPoint] = []
// let gridValues: [CGFloat] = [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.05]
for (index, value) in entries.enumerated() {
let difference: CGFloat = 0.125 / 30
let userValue: CGFloat = isSystolic ? CGFloat(value.systolic) : CGFloat(value.diastolic)
var height = (userValue - 30.0) * difference
height = (1.0 - height) * gridLayer.frame.size.height
let point = CGPoint(x: CGFloat(index)*lineGap + 40, y: height)
result.append(point)
}
return result
}
/// Draw a zigzag line connecting all points in dataPoints
private func drawChart(for points: [CGPoint]?, color: UIColor) {
if let dataPoints = points, dataPoints.count > 0 {
guard let path = createPath(for: points) else { return }
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.strokeColor = color.cgColor
lineLayer.fillColor = UIColor.clear.cgColor
dataLayer.addSublayer(lineLayer)
}
}
/// Create a zigzag bezier path that connects all points in dataPoints
private func createPath(for points: [CGPoint]?) -> UIBezierPath? {
guard let dataPoints = points, dataPoints.count > 0 else {
return nil
}
let path = UIBezierPath()
path.move(to: dataPoints[0])
for i in 1..<dataPoints.count {
path.addLine(to: dataPoints[i])
}
return path
}
/// Create titles at the bottom for all entries showed in the chart
private func drawLables() {
if let dataEntries = dataEntries,
dataEntries.count > 0 {
for i in 0..<dataEntries.count {
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: lineGap*CGFloat(i) - lineGap/2 + 40, y: mainLayer.frame.size.height - bottomSpace/2 - 8, width: lineGap, height: 16)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 11
textLayer.string = dataEntries[i].label
mainLayer.addSublayer(textLayer)
}
}
}
/// Create horizontal lines (grid lines) and show the value of each line
private func drawHorizontalLines() {
let gridValues: [CGFloat] = [1.05, 1.0, 0.875, 0.75, 0.625, 0.5, 0.375, 0.25, 0.125]
let gridText = ["", "30", "60", "90", "120", "150", "180", "210", "240"]
for (index, value) in gridValues.enumerated() {
let height = value * gridLayer.frame.size.height
let path = UIBezierPath()
if value == gridValues.first! {
path.move(to: CGPoint(x: 30, y: height))
} else {
path.move(to: CGPoint(x: 28, y: height))
}
path.addLine(to: CGPoint(x: gridLayer.frame.size.width, y: height))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.black.cgColor
if value != gridValues.first! {
lineLayer.lineDashPattern = [4, 4]
}
lineLayer.lineWidth = 0.5
gridLayer.addSublayer(lineLayer)
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 4, y: height-8, width: 50, height: 16)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 12
textLayer.string = gridText[index]
gridLayer.addSublayer(textLayer)
}
}
private func drawVerticleLine() {
let height = gridLayer.frame.size.height * 1.05
let path = UIBezierPath()
path.move(to: CGPoint(x: 30, y: 0))
path.addLine(to: CGPoint(x: 30, y: height))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.black.cgColor
lineLayer.lineWidth = 0.5
gridLayer.addSublayer(lineLayer)
}
private func clean() {
mainLayer.sublayers?.forEach({
if $0 is CATextLayer {
$0.removeFromSuperlayer()
}
})
dataLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
gridLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
}
}
How can I add shadow to lines like shown in the first image and add simple line drawing animation to the Graph?

Swift 4 - Create UIView with 2 background colors

I have a UIView having equal width/height as of its superview. This is what I want to get.
For the time being I have used a static background image. Any ideas?
Result
Code
import UIKit
#IBDesignable
public class AngleView: UIView {
#IBInspectable public var fillColor: UIColor = .blue { didSet { setNeedsLayout() } }
var points: [CGPoint] = [
.zero,
CGPoint(x: 1, y: 0),
CGPoint(x: 1, y: 0.4),
CGPoint(x: 0, y: 0.5)
] { didSet { setNeedsLayout() } }
private lazy var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
self.layer.insertSublayer(_shapeLayer, at: 0)
return _shapeLayer
}()
override public func layoutSubviews() {
shapeLayer.fillColor = fillColor.cgColor
guard points.count > 2 else {
shapeLayer.path = nil
return
}
let path = UIBezierPath()
path.move(to: convert(relativePoint: points[0]))
for point in points.dropFirst() {
path.addLine(to: convert(relativePoint: point))
}
path.close()
shapeLayer.path = path.cgPath
}
private func convert(relativePoint point: CGPoint) -> CGPoint {
return CGPoint(x: point.x * bounds.width + bounds.origin.x, y: point.y * bounds.height + bounds.origin.y)
}
}
Source
Just modified the values :D
swift 4: you can instead ,of making a view with several background colors, add sublayer to your view.
//connect your view.
#IBOutlet weak var yourView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
//add a shape
let shape = CAShapeLayer()
self.yourView.layer.addSublayer(shape)
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.black.cgColor
// add your path
// you can simply adjust the points.
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: (3*yourView.bounds.height)/4))
path.addLine(to: CGPoint(x:self.yourView.bounds.width , y: yourView.bounds.height/2))
path.addLine(to: CGPoint(x: self.yourView.bounds.width, y: 0))
path.close()
shape.path = path.cgPath
}