Ok, probably this question has been asked 1000 times on SO. And I must have tried 500 of the answers. Nothing works, which makes me think this might not be possible.
Before I even start, I don't want a animable custom property, I just want to draw on bounds change. Most of the answers treat the former only. But what if my content depends only on the size of the layer and nothing more?
Is this really not possible? I tried:
override class func needsDisplay(forKey key: String) -> BoolneedsDisplayOnBoundsChange- overriding every kind of
action(for:)oranimation(for:)method contentMode = .redraw- tons of hacks like overriding
setBoundsand adding an implicit animation (which is useless btw since the layer has an animation, it just doesn't redraw on resize) - maybe a couple of other things I forgot, spent quite a while on this
The furthest I've reached: the contents draw at the beginning of the animation and then at the end. Which of course looks bad (also pixelated) when going from large to small.
Not necessarily important, but here is the draw method for reference:
override func draw(in ctx: CGContext) {
UIGraphicsPushContext(ctx)
paths = [Dimension: [UIBezierPath]]()
cuts = [Dimension: [UIBezierPath]]()
let centerTranslate = CGAffineTransform.init(translationX: frame.width * 0.5,
y: frame.height * 0.5)
let dimRotateAndTranslate = centerTranslate.rotated(by: CGFloat.pi / 2)
let needlePath = UIBezierPath(rect: CGRect(x: 0,
y: 0,
width: circularSpacing,
height: frame.height * 0.5 - centerRadius))
let piProgress = CGFloat(progress * 2 * Float.pi)
var needleRotateAndTranslate = CGAffineTransform(rotationAngle: piProgress)
needleRotateAndTranslate = needleRotateAndTranslate.translatedBy(x: -needlePath.bounds.width * 0.5,
y: -needlePath.bounds.height - centerRadius)
needlePath.apply(needleRotateAndTranslate.concatenating(centerTranslate))
let sortedDims = states.sorted(by: {$0.key < $1.key})
for (offset: i, element: (key: dim, value: beats)) in sortedDims.enumerated() {
var pathsAtDim = paths[dim] ?? []
var cutAtDim = cuts[dim] ?? []
let thickness = (frame.width * 0.5 - centerRadius - circularSpacing * CGFloat(states.count - 1)) / CGFloat(states.count)
let xSpacing = thickness + circularSpacing
let radius = centerRadius + thickness * 0.5 + CGFloat(i) * xSpacing
for (offset: j, beat) in beats.enumerated() {
let length = CGFloat.pi * 2 / CGFloat(beats.count) * CGFloat(beat.relativeDuration)
var path = UIBezierPath(arcCenter: .zero,
radius: radius,
startAngle: CGFloat(j) * length,
endAngle: CGFloat(j + 1) * length,
clockwise: true)
path = UIBezierPath(cgPath: path.cgPath.copy(strokingWithWidth: thickness,
lineCap: .butt,
lineJoin: .miter,
miterLimit: 0,
transform: dimRotateAndTranslate))
func cutAtIndex(index: Int, offset: CGFloat) -> UIBezierPath {
let cut = UIBezierPath(rect: CGRect(x: 0,
y: 0,
width: thickness,
height: circularSpacing))
let offsetRadians = offset / radius
let x = (radius - thickness / 2) * cos(CGFloat(index) * length + offsetRadians)
let y = (radius - thickness / 2) * sin(CGFloat(index) * length + offsetRadians)
let translate = CGAffineTransform.init(translationX: x,
y: y)
let rotate = CGAffineTransform.init(rotationAngle: CGFloat(index) * length)
cut.apply(rotate
.concatenating(translate)
.concatenating(dimRotateAndTranslate))
return cut
}
cutAtDim.append(cutAtIndex(index: j, offset: -circularSpacing * 0.5))
pathsAtDim.append(path)
}
paths[dim] = pathsAtDim
cuts[dim] = cutAtDim
}
let clippingMask = UIBezierPath.init(rect: bounds)
for (dim, _) in states {
for cut in cuts[dim] ?? [] {
clippingMask.append(cut.reversing())
}
}
clippingMask.addClip()
for (dim, states) in states {
for i in 0..<states.count {
let state = states[i]
if let path = paths[dim]?[i] {
let color = dimensionsColors[dim]?[state.state] ?? .darkGray
color.setFill()
path.fill()
}
}
}
ctx.resetClip()
needleColor.setFill()
needlePath.fill()
UIGraphicsPopContext()
}

