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)

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With