import Foundation import UIKit import Display final class SemanticStatusNodeProgressTransition { let beginTime: Double let initialValue: CGFloat init(beginTime: Double, initialValue: CGFloat) { self.beginTime = beginTime self.initialValue = initialValue } func valueAt(timestamp: Double, actualValue: CGFloat) -> (CGFloat, Bool) { let duration = 0.2 var t = CGFloat((timestamp - self.beginTime) / duration) t = min(1.0, max(0.0, t)) return (t * actualValue + (1.0 - t) * self.initialValue, t >= 1.0 - 0.001) } } final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { let transitionFraction: CGFloat let value: CGFloat? let displayCancel: Bool let appearance: SemanticStatusNodeState.ProgressAppearance? let timestamp: Double init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) { self.transitionFraction = transitionFraction self.value = value self.displayCancel = displayCancel self.appearance = appearance self.timestamp = timestamp super.init() } func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { let diameter = size.width let factor = diameter / 50.0 context.saveGState() if foregroundColor.alpha.isZero { context.setBlendMode(.destinationOut) context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) } else { context.setBlendMode(.normal) context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) } var progress: CGFloat var startAngle: CGFloat var endAngle: CGFloat if let value = self.value { progress = value startAngle = -CGFloat.pi / 2.0 endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle if progress > 1.0 { progress = 2.0 - progress let tmp = startAngle startAngle = endAngle endAngle = tmp } progress = min(1.0, progress) } else { progress = CGFloat(1.0 + self.timestamp.remainder(dividingBy: 2.0)) startAngle = -CGFloat.pi / 2.0 endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle if progress > 1.0 { progress = 2.0 - progress let tmp = startAngle startAngle = endAngle endAngle = tmp } progress = min(1.0, progress) } let lineWidth: CGFloat if let appearance = self.appearance { lineWidth = appearance.lineWidth } else { lineWidth = max(1.6, 2.25 * factor) } let pathDiameter: CGFloat if let appearance = self.appearance { pathDiameter = diameter - lineWidth - appearance.inset * 2.0 } else { pathDiameter = diameter - lineWidth - 2.5 * 2.0 } var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0) angle *= 4.0 context.translateBy(x: diameter / 2.0, y: diameter / 2.0) context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0))) context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0) let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) path.lineWidth = lineWidth path.lineCapStyle = .round path.stroke() context.restoreGState() if self.displayCancel { if foregroundColor.alpha.isZero { context.setBlendMode(.destinationOut) context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) } else { context.setBlendMode(.normal) context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) } context.saveGState() context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction)) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) context.setLineWidth(max(1.3, 2.0 * factor)) context.setLineCap(.round) let crossSize: CGFloat = 14.0 * factor context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) context.strokePath() context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) context.strokePath() context.restoreGState() } } } var value: CGFloat? let displayCancel: Bool let appearance: SemanticStatusNodeState.ProgressAppearance? var transition: SemanticStatusNodeProgressTransition? var isAnimating: Bool { return true } var requestUpdate: () -> Void = {} init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) { self.value = value self.displayCancel = displayCancel self.appearance = appearance } func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { let timestamp = CACurrentMediaTime() let resolvedValue: CGFloat? if let value = self.value { if let transition = self.transition { let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value) resolvedValue = v if isCompleted { self.transition = nil } } else { resolvedValue = value } } else { resolvedValue = nil } return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp) } func maskView() -> UIView? { return nil } func updateValue(value: CGFloat?) { if value != self.value { let previousValue = self.value self.value = value let timestamp = CACurrentMediaTime() if let _ = value, let previousValue = previousValue { if let transition = self.transition { self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue).0) } else { self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue) } } else { self.transition = nil } } } }