How do you trace the border of an iPhone screen (including the notch for an X, 11, or 12 series)? - swift

I'm trying to trace a line around the outside of the main view - trace along the edge of the screen. The following image shows the outline, and the code below shows how it animates.
The problem is, the animation draws the outer edge first, and then it draws the area around the notch.
I need it to start to draw the outer edge, drop down and draw around the notch, and then continue along the outer edge until it finishes.
import UIKit
class ViewController: UIViewController {
var layer: CAShapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
setupBorder()
animateBorder()
}
func setupBorder() {
let bounds = self.view.bounds
if UIDevice.current.hasNotch {
// FIXME: needs tweaks for:
// 12 pro max (corners and notch)
// 12 pro (corners)
// 12 mini (notch)
// the math works for all X and 11 series
// border around the phone screen
let framePath = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 40, height: 40))
// Math courtesy of:
// https://www.paintcodeapp.com/news/iphone-x-screen-demystified
let devicePointWidth = UIScreen.main.bounds.size.width
let w = devicePointWidth * 83 / 375
let n = devicePointWidth * 209 / 375
let notchBounds = CGRect(x: w, y: -10, width: n, height: 40)
let notchPath = UIBezierPath(roundedRect: notchBounds, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 20, height: 20))
// This is the problem. The framePath is drawn first,
// and then the notchPath is drawn. I need these to be
// mathematically merged
framePath.append(notchPath)
framePath.usesEvenOddFillRule = true
layer.path = framePath.cgPath
} else {
// if device is an 8 or lower, the border of the screen
// is a rectangle
layer.path = UIBezierPath(rect: bounds).cgPath
}
layer.strokeColor = UIColor.blue.cgColor
layer.strokeEnd = 0.0
layer.lineWidth = 20.0
layer.fillColor = nil
self.view.layer.addSublayer(layer)
}
func animateBorder() {
CATransaction.begin()
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 6
CATransaction.setCompletionBlock { [weak self] in
self?.layer.strokeColor = UIColor.cyan.cgColor
self?.layer.strokeEnd = 1.0
}
layer.add(animation, forKey: "stroke-screen")
CATransaction.commit()
}
}
extension UIDevice {
var hasNotch: Bool {
// FIXME: Does not work with apps that use SceneDelegate
// Requires the window var in the AppDelegate
if #available(iOS 11.0, *) {
return UIApplication.shared.delegate?.window??.safeAreaInsets.bottom ?? 0 > 20
}
return false
}
}
The code above can be used in a new project, BUT the SceneDelegate.swift file will need to be removed, the Application Scene Manifest entry in the Info.plist will need to be deleted, and var window: UIWindow? will need to be added to AppDelegate.swift.

You're having that issue because you're creating two separate shapes and appending one to the next. So your path is being properly drawn.
You need to create one shape by using the exact coordinates so its properly drawn.
Here's a simple shape without any rounded corners:
let framePath = UIBezierPath()
framePath.move(to: .zero)
framePath.addLine(to: CGPoint(x: w, y: 0))
framePath.addLine(to: CGPoint(x: w, y: 30))
framePath.addLine(to: CGPoint(x: w+n, y: 30))
framePath.addLine(to: CGPoint(x: w+n, y: 0))
framePath.addLine(to: CGPoint(x: devicePointWidth, y: 0))
framePath.addLine(to: CGPoint(x: devicePointWidth, y: self.view.bounds.height))
framePath.addLine(to: CGPoint(x: 0, y: self.view.bounds.height))
framePath.addLine(to: .zero)
Then you can user addArc(withCenter:radius:startAngle:endAngle:clockwise:) to add the curved parts.
Here is a rough draft using some of the values that you had calculated:
let circleTop = CGFloat(3*Double.pi / 2)
let circleRight = CGFloat(0)
let circleBottom = CGFloat(Double.pi / 2)
let circleLeft = CGFloat(Double.pi)
let framePath = UIBezierPath()
framePath.move(to: CGPoint(x: 40, y: 0))
framePath.addLine(to: CGPoint(x: w-6, y: 0))
framePath.addArc(withCenter: CGPoint(x: w-6, y: 6), radius: 6, startAngle: circleTop, endAngle: circleRight, clockwise: true)
framePath.addArc(withCenter: CGPoint(x: w+20, y: 10), radius: 20, startAngle: circleLeft, endAngle: circleBottom, clockwise: false)
framePath.addLine(to: CGPoint(x: w+n-20, y: 30))
framePath.addArc(withCenter: CGPoint(x: w+n-20, y: 10), radius: 20, startAngle: circleBottom, endAngle: circleRight, clockwise: false)
framePath.addArc(withCenter: CGPoint(x: w+n+6, y: 6), radius: 6, startAngle: CGFloat(Double.pi), endAngle: circleTop, clockwise: true)
framePath.addLine(to: CGPoint(x: devicePointWidth-40, y: 0))
framePath.addArc(withCenter: CGPoint(x: devicePointWidth-40, y: 40), radius: 40, startAngle: circleTop, endAngle: circleRight, clockwise: true)
framePath.addLine(to: CGPoint(x: devicePointWidth, y: self.view.bounds.height - 40))
framePath.addArc(withCenter: CGPoint(x: UIScreen.main.bounds.size.width-40, y: UIScreen.main.bounds.size.height-40), radius: 40, startAngle: circleRight, endAngle: circleBottom, clockwise: true)
framePath.addLine(to: CGPoint(x: 40, y: self.view.bounds.height))
framePath.addArc(withCenter: CGPoint(x: 40, y: UIScreen.main.bounds.size.height-40), radius: 40, startAngle: circleBottom, endAngle: circleLeft, clockwise: true)
framePath.addLine(to: CGPoint(x: 0, y: 40))
framePath.addArc(withCenter: CGPoint(x: 40, y: 40), radius: 40, startAngle: circleLeft, endAngle: circleTop, clockwise: true)

Related

Draw semi circle with bottom line to use it as skshapenode in spritekit

I want to draw a semi circle with the bottom line and reuse it as shape node again. How can I implement this. This is my code for semi circle.
And also some code is hard coded so some help is appreciated.
func drawLine(from: CGPoint, to: CGPoint) {
// for line
let myLine = SKShapeNode()
let myPath = CGMutablePath()
myPath.addLines(between: [from, to])
myLine.path = myPath
myLine.strokeColor = SKColor.blue
myLine.lineWidth = 4
addChild(myLine)
}
func drawSemi(){
// for semi circle
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 50, startAngle: 0, endAngle: .pi, clockwise: true)
let pathNode = SKShapeNode(path: bezierPath.cgPath)
pathNode.strokeColor = SKColor.blue
pathNode.position = CGPoint(x: self.size.width/2, y: self.size.height/2)
pathNode.lineWidth = 3
addChild(pathNode)
}
func drawCompleteSemi(){
drawSemi()
drawLine(from: CGPoint(x: 450, y: 500), to: CGPoint(x: 550, y: 500))
}
simply call close() on your bezier path, and omit the drawline function entirely
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 50, startAngle: 0, endAngle: .pi, clockwise: true)
bezierPath.close() //<-- add this line

Need to create custom shape for UIView Swift

I need to create this custom UIView for my project.I need to achieve this
I was doing trial and error by creating, My own custom UIView and then drawing on it.
I'm working on this
My Question is, if I'm able to draw my custom shape, How will I clip-off the remaining white space of the UIView
func drawCustomShape() {
let viewSize = self.frame.size
let buttonSlotRadius = CGFloat(30)
let effectiveViewHeight = viewSize.height - buttonSlotRadius
let path = UIBezierPath()
path.move(to: CGPoint(x: 30, y: 0))
path.addLine(to: CGPoint(x: viewSize.width - 30, y: 0))
path.addArc(withCenter: CGPoint(x: viewSize.width - 30,
y: 30),
radius: 30,
startAngle: .pi * 3 / 2,
endAngle: 0,
clockwise: true)
path.addLine(to: CGPoint(x: viewSize.width, y: effectiveViewHeight))
path.addArc(withCenter: CGPoint(x: viewSize.width - 30,
y: effectiveViewHeight - 30),
radius: 30,
startAngle: 0,
endAngle: .pi / 2,
clockwise: true)
path.addLine(to: CGPoint(x: viewSize.width / 4, y: effectiveViewHeight))
// close path join to origin
path.close()
UIColor.secondarySystemFill.setFill()
path.fill()
path.lineWidth = 6.0
path.stroke()
}
You can use UIView with clear background color, and use CAShapeLayer with desired path like this:
extension UIView {
func drawCustomShape() {
let cornerRadius: CGFloat = 18.0
let shapeOffset = self.frame.size.height * 0.2
//create shape layer
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.lineWidth = 1.0
shapeLayer.fillColor = UIColor.white.cgColor
self.layer.addSublayer(shapeLayer)
//create path
let path = UIBezierPath()
//top left point
path.move(to: CGPoint(x: 0, y: cornerRadius))
//top left corner
path.addQuadCurve(to: CGPoint(x: cornerRadius, y: 0),
controlPoint: CGPoint(x: 0, y: 0))
//top right point
path.addLine(to: CGPoint(x: self.frame.size.width - cornerRadius, y: 0))
//top right corner
path.addQuadCurve(to: CGPoint(x: self.frame.size.width, y: cornerRadius),
controlPoint: CGPoint(x: self.frame.size.width, y: 0))
//bottom right point
path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height - shapeOffset - cornerRadius))
//bottom right corner
path.addQuadCurve(to: CGPoint(x: self.frame.size.width - cornerRadius, y: self.frame.size.height - shapeOffset),
controlPoint: CGPoint(x: self.frame.size.width, y: self.frame.size.height - shapeOffset))
//bottom left
path.addLine(to: CGPoint(x: cornerRadius, y: self.frame.size.height))
//bottom left corner
path.addQuadCurve(to: CGPoint(x: 0, y: self.frame.size.height - cornerRadius),
controlPoint: CGPoint(x: 0, y: self.frame.size.height))
path.close()
shapeLayer.path = path.cgPath
}
Usage:
myView.drawCustomShape()
Result:

How to draw custom shape with arc in centre using UIBezierPath

I am try create custom shape of view with arc in centre, but I am not working with this tools in past.
I create shape what I want but not elegant like need.
What I do wrong?
I am feel where I do mistakes but can't fix it.
How work with this tools, I use this tutorial - https://ayusinghi96.medium.com/draw-custom-shapes-and-views-with-uiberzierpath-ios-1737f5cb975
Result which need:
My result:
My code:
import Foundation
import UIKit
/// Custom card view class
///
class CardView : UIView
{
// init the view with a rectangular frame
override init(frame: CGRect)
{
super.init(frame: frame)
backgroundColor = UIColor.clear
}
// init the view by deserialisation
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
backgroundColor = UIColor.clear
}
/// override the draw(_:) to draw your own view
///
/// Default implementation - `rectangular view`
///
override func draw(_ rect: CGRect)
{
let padding: CGFloat = 5.0
let centerButtonHeight: CGFloat = 50
let f = CGFloat(centerButtonHeight / 2.0) + padding
let halfW = frame.width/2.0
let r = CGFloat(11)
// Card view corner radius
let cardRadius = CGFloat(7)
// Button slot arc radius
let buttonSlotRadius = CGFloat(30)
// Card view frame dimensions
let viewSize = self.bounds.size
// Effective height of the view
let effectiveViewHeight = viewSize.height - buttonSlotRadius
// Get a path to define and traverse
let path = UIBezierPath()
// Shift origin to left corner of top straight line
path.move(to: CGPoint(x: cardRadius, y: 0))
// top line
path.addLine(to: CGPoint(x: viewSize.width - cardRadius, y: 0))
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/7.0)), radius: f, startAngle: .pi, endAngle: 0, clockwise: false)
//
path.addQuadCurve(to: CGPoint(x: halfW+f+(r/5.5), y: 0), controlPoint: CGPoint(x: halfW+f, y: 0))
// top-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: cardRadius
),
radius: cardRadius,
startAngle: CGFloat(Double.pi * 3 / 2),
endAngle: CGFloat(0),
clockwise: true
)
// right line
path.addLine(
to: CGPoint(x: viewSize.width, y: effectiveViewHeight)
)
// bottom-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius,
startAngle: CGFloat(0),
endAngle: CGFloat(Double.pi / 2),
clockwise: true
)
// right half of bottom line
path.addLine(
to: CGPoint(x: viewSize.width / 4 * 3, y: effectiveViewHeight)
)
// left half of bottom line
path.addLine(
to: CGPoint(x: cardRadius, y: effectiveViewHeight)
)
// bottom-left corner arc
path.addArc(
withCenter: CGPoint(
x: cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius,
startAngle: CGFloat(Double.pi / 2),
endAngle: CGFloat(Double.pi),
clockwise: true
)
// left line
path.addLine(to: CGPoint(x: 0, y: cardRadius))
// top-left corner arc
path.addArc(
withCenter: CGPoint(x: cardRadius, y: cardRadius),
radius: cardRadius,
startAngle: CGFloat(Double.pi),
endAngle: CGFloat(Double.pi / 2 * 3),
clockwise: true
)
// close path join to origin
path.close()
// Set the background color of the view
UIColor.init(red: 0.118, green: 0.118, blue: 0.15, alpha: 1).set()
path.fill()
}
}
You need to change these two lines:
path.addArc(withCenter: CGPoint(x: halfW, y: (r/7.0)), radius: f, startAngle: .pi, endAngle: 0, clockwise: false)
//
path.addQuadCurve(to: CGPoint(x: halfW+f+(r/5.5), y: 0), controlPoint: CGPoint(x: halfW+f, y: 0))
to:
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), y: 0), controlPoint: CGPoint(x: halfW+f, y: 0))
And here is the fixed and slightly refactored code inside the func draw(_ rect: CGRect)
override func draw(_ rect: CGRect) {
let padding: CGFloat = 5
let centerButtonHeight: CGFloat = 50
let f: CGFloat = centerButtonHeight / 2 + padding
let halfW = frame.width / 2
let r: CGFloat = 11
// Card view corner radius
let cardRadius: CGFloat = 7
// Button slot arc radius
let buttonSlotRadius: CGFloat = 30
// Card view frame dimensions
let viewSize = self.bounds.size
// Effective height of the view
let effectiveViewHeight = viewSize.height - buttonSlotRadius
// Get a path to define and traverse
let path = UIBezierPath()
// Shift origin to left corner of top straight line
path.move(to: CGPoint(x: cardRadius, y: 0))
// top line
path.addLine(to: CGPoint(x: viewSize.width - cardRadius, y: 0))
path.addLine(to: CGPoint(x: halfW - f - (r / 2), y: 0))
path.addQuadCurve(
to: CGPoint(x: halfW - f, y: (r / 2)),
controlPoint: CGPoint(x: halfW - f, y: 0)
)
path.addArc(
withCenter: CGPoint(x: halfW, y: (r / 2)),
radius: f, startAngle: .pi, endAngle: 0, clockwise: false
)
//
path.addQuadCurve(
to: CGPoint(x: halfW + f + (r / 2), y: 0),
controlPoint: CGPoint(x: halfW + f, y: 0)
)
// top-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: cardRadius
),
radius: cardRadius, startAngle: .pi * 3 / 2, endAngle: 0, clockwise: true
)
// right line
path.addLine(
to: CGPoint(x: viewSize.width, y: effectiveViewHeight)
)
// bottom-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius, startAngle: 0, endAngle: .pi / 2, clockwise: true
)
// right half of bottom line
path.addLine(
to: CGPoint(x: viewSize.width / 4 * 3, y: effectiveViewHeight)
)
// left half of bottom line
path.addLine(
to: CGPoint(x: cardRadius, y: effectiveViewHeight)
)
// bottom-left corner arc
path.addArc(
withCenter: CGPoint(
x: cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius, startAngle: .pi / 2, endAngle: .pi, clockwise: true
)
// left line
path.addLine(to: CGPoint(x: 0, y: cardRadius))
// top-left corner arc
path.addArc(
withCenter: CGPoint(x: cardRadius, y: cardRadius),
radius: cardRadius, startAngle: .pi, endAngle: .pi / 2 * 3, clockwise: true
)
// close path join to origin
path.close()
// Set the background color of the view
UIColor(red: 0.118, green: 0.118, blue: 0.15, alpha: 1).set()
path.fill()
}

How to make the UIView as Like Ticket in UITablevViewCell

TicketViewTableViewCell.swift
#IBOutlet weak var ticketView: TestView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
ticketView.layer.masksToBounds = true
ticketView.layer.cornerRadius = 8.0
ticketView.circleRadius = 10
ticketView.circleY = ticketView.frame.height * 0.6
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
enter image description here
I want to get the UIView like the above image in TableViewCell.
You can use this custom view as a background view:
#IBDesignable class TicketBackground: UIView {
#IBInspectable var cornerRadius : CGFloat = 4
#IBInspectable var indentRadius : CGFloat = 10
#IBInspectable var color : UIColor = UIColor.lightGray
override func draw(_ rect: CGRect) {
drawBackground()
}
private func drawBackground()
{
self.backgroundColor = UIColor.clear
let width = self.frame.width
let height = self.frame.height
let path = UIBezierPath()
path.move(to: CGPoint(x: cornerRadius, y: 0))
path.addLine(to: CGPoint(x: width-cornerRadius, y: 0))
path.addArc(withCenter: CGPoint(x: width-cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: 3*CGFloat.pi/2, endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: width, y: height/2-indentRadius))
path.addArc(withCenter: CGPoint(x: width, y: height/2), radius: indentRadius, startAngle: 3*CGFloat.pi/2, endAngle: CGFloat.pi/2, clockwise: false)
path.addLine(to: CGPoint(x: width, y: height-cornerRadius))
path.addArc(withCenter: CGPoint(x: width-cornerRadius, y: height-cornerRadius), radius: cornerRadius, startAngle: 0, endAngle: CGFloat.pi/2, clockwise: true)
path.addLine(to: CGPoint(x: cornerRadius, y: height))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: height-cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi/2, endAngle: CGFloat.pi, clockwise: true)
path.addLine(to: CGPoint(x: 0, y: height/2+indentRadius))
path.addArc(withCenter: CGPoint(x: 0, y: height/2), radius: indentRadius, startAngle: CGFloat.pi/2, endAngle: 3*CGFloat.pi/2, clockwise: false)
path.addLine(to: CGPoint(x: 0, y: cornerRadius))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: 3*CGFloat.pi/2, clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = self.color.cgColor
self.layer.addSublayer(shapeLayer)
}
}

How can I prevent closing Bezier Path

I want to create a Bezier curve, and create a collision boundary for my object.
let firstSurfacePoint = CGPoint(x: 30, y: 120)
let secondSurfacePoint = CGPoint(x: 20, y: 200)
let thirdSurfacePoint = CGPoint(x: 200, y: 300)
let center = CGPoint(x: 150, y: 120)
let radius: CGFloat = 120.00
let arcWidth: CGFloat = 20.00
let fourthSurfacePoint = CGPoint(x: 240, y: 300)
func createCollisionPath(collision: UICollisionBehavior) {
let line = UIBezierPath()
line.moveToPoint(firstSurfacePoint)
line.addCurveToPoint(thirdSurfacePoint, controlPoint1: secondSurfacePoint, controlPoint2: secondSurfacePoint)
line.addLineToPoint(fourthSurfacePoint)
let currentPath = CAShapeLayer()
currentPath.path = line.CGPath
currentPath.strokeColor = UIColor.blackColor().CGColor
currentPath.fillColor = UIColor.clearColor().CGColor
currentPath.lineWidth = 1
view.layer.addSublayer(currentPath)
collision.addBoundaryWithIdentifier("curve", forPath: line)
}
the same poor result I get, if I choose addArcWithCente
line.moveToPoint(firstSurfacePoint)
line.addArcWithCenter(center, radius: radius, startAngle: CGFloat(M_PI), endAngle: CGFloat(M_PI/2), clockwise: false)
line.addLineToPoint(fourthSurfacePoint)
In this two tries receive a strange results, that are crucial, as I can't create a proper collision. In the first attempt my object goes through path as if it is a straight line from firstPoint to thirdPoint, as well as in the second
What am I doing wrong?
My figure(without Arc)
My figure(with Arc)
currentPath.fillColor = nil
or
currentPath.fillColor = UIColor.clearColor().CGColor