Search code examples
swiftcalayer

swift circle Irregular layer


My app want to implement circle irregular animation

So I convert Custom anime layer to swift 4

But click button nothing gonna happen no error no crash

I have no idea how to fix the problem

code here

class CustomLayer: CALayer {
var progress: CGFloat = 0
let radius: CGFloat = 80
let lineWidth: CGFloat = 6.0
let xScale: CGFloat = 1.2
let yScale: CGFloat = 0.8
let controlPointFactor: CGFloat = 1.8
let pointRadius: CGFloat = 3.0

// MARK: - init
override init(layer: Any) {
    super.init(layer: layer)
    let theLayer = layer as! CustomLayer
    progress = theLayer.progress
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

override init() {
    super.init()
}

// MARK: - needsDisplayForKey
override static func needsDisplay(forKey key: String) -> Bool {
    switch key {
    case "progress":
        return true
    default:
        break
    }

    return super.needsDisplay(forKey: key)
}

// MARK: - draw
override func draw(in ctx: CGContext) {
    let path = UIBezierPath()
    // 以底点为原点
    let bottom = CGPoint(x: bounds.midX, y: bounds.midY + radius)
    // 控制点偏移距离
    let controlOffsetDistance = radius / controlPointFactor

    // 各点变化系数
    let xFactor = 1 + (xScale - 1) * progress
    let yFactor = 1 - (1 - yScale) * progress
    // 顶点特殊的变化系数(破坏规则变形)
    let topYFactor = 1 - (1 - yScale) * progress * 1.5

    // 右上弧
    let origin0 = CGPoint(x: bottom.x + radius * xFactor, y: bottom.y - radius * yFactor)
    let dest0 = CGPoint(x: bottom.x, y: bottom.y - radius * 2 * topYFactor)
    let control0A = CGPoint(x: origin0.x, y: origin0.y - controlOffsetDistance)
    let control0B = CGPoint(x: dest0.x + controlOffsetDistance, y: bottom.y - radius * 2 * yFactor)
    path.move(to: origin0)
    path.addCurve(to: dest0, controlPoint1: control0A, controlPoint2: control0B)
    // 左上弧
    let origin1 = dest0
    let dest1 = CGPoint(x: bottom.x - radius * xFactor, y: bottom.y - radius * yFactor)
    let control1A = CGPoint(x: origin1.x - controlOffsetDistance, y: origin0.y - radius * 2 * yFactor)
    let control1B = CGPoint(x: dest1.x, y: dest1.y - controlOffsetDistance)
    path.addCurve(to: dest1, controlPoint1: control1A, controlPoint2: control1B)

    // 左下弧
    let origin2 = dest1
    let dest2 = bottom
    let control2A = CGPoint(x: origin2.x, y: origin2.y + controlOffsetDistance)
    let control2B = CGPoint(x: dest2.x - controlOffsetDistance, y: dest2.y)
    path.addCurve(to: dest2, controlPoint1: control2A, controlPoint2: control2B)

    // 右下弧
    let origin3 = dest2
    let dest3 = origin0
    let control3A = CGPoint(x: origin3.x + controlOffsetDistance, y: origin3.y)
    let control3B = CGPoint(x: dest3.x, y: dest3.y + controlOffsetDistance)
    path.addCurve(to: dest3, controlPoint1: control3A, controlPoint2: control3B)

    ctx.addPath(path.cgPath)

    ctx.setLineWidth(lineWidth)
    ctx.setStrokeColor(UIColor.blue.cgColor)
    ctx.strokePath()

}

// MARK: - tools
private func addArcForPath(path: UIBezierPath, point: CGPoint) {
    path.move(to: point)
    path.addArc(withCenter: point, radius: pointRadius, startAngle: 0, endAngle: CGFloat(.pi * 2.0), clockwise: true)
}
}

ViewController Class

class ViewController: UIViewController {

let startButton: UIButton = {
    let button = UIButton(type: .system)
    button.setTitle("Start animation", for: .normal)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
}()

let circleView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.clipsToBounds = true
    return view
}()

var animationLayer: CustomLayer!

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .white
    view.addSubview(startButton)
    startButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    startButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
    startButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
    startButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
    startButton.addTarget(self, action: #selector(startAnimation), for: .touchUpInside)
    view.addSubview(circleView)
    circleView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    circleView.topAnchor.constraint(equalTo: startButton.bottomAnchor, constant: 50).isActive = true
    circleView.widthAnchor.constraint(equalToConstant: 200).isActive = true
    circleView.heightAnchor.constraint(equalToConstant: 200).isActive = true
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    addAnimationLayer()
}

private func addAnimationLayer() {
    animationLayer = CustomLayer()
    animationLayer.contentsScale = UIScreen.main.scale
    animationLayer.frame = circleView.bounds
    animationLayer.progress = 0;
    circleView.layer.addSublayer(animationLayer)
}

@objc func startAnimation() {
    animationLayer.removeAllAnimations()

    // end status
    animationLayer.progress = 1;

    // animation
    let animation = CABasicAnimation(keyPath: "progress")
    animation.duration = 2
    animation.fromValue = 0.0
    animation.toValue = 1.0
    animationLayer.add(animation, forKey: nil)
}
}

Objective-C version is work but I need swift version

Can give me some hint or Is something I missing?


Solution

  • So I had a few issues. The first is the use of viewDidLayoutSubviews. It was creating and adding a number of layers. I'd prefer a better solution, but the best I could come up with was just using a Bool flag to indicate when I'd actually added the layer, for example...

    var layerAdded: Bool = false
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        guard !layerAdded else {
            return
        }
        layerAdded = true
        addAnimationLayer()
    }
    

    The next issue took a little bit more effort and was finally solved with the help of Subclassing CALayer + Implicit Animation w/ Swift3

    First, I had to change needsDisplay(forKey:) to support the #keyPath directive...

    // MARK: - needsDisplayForKey
    override static func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(progress) {
            print("needsDisplay for \(key)")
            return true
        }
    
        return super.needsDisplay(forKey: key)
    }
    

    which led to having to change progress to...

    @NSManaged var progress: CGFloat
    

    which included adding defaultValue(forKey:)...

    override class func  defaultValue(forKey key: String) -> Any? {
        if key == #keyPath(progress) {
            return 1.0
        }
        else {
            return super.defaultValue(forKey: key)
        }
    }
    

    After I did all that, it worked :/