3

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.

iPhone border

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.

Jeffrey Berthiaume
  • 4,054
  • 3
  • 35
  • 45

1 Answers1

6

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)

enter image description here

valosip
  • 3,167
  • 1
  • 14
  • 26