// // StripProgressView.swift // Mastodon // // Created by MainasuK Cirno on 2021-3-3. // import os.log import UIKit import Combine private final class StripProgressLayer: CALayer { var tintColor: UIColor = .black @NSManaged var progress: CGFloat override class func needsDisplay(forKey key: String) -> Bool { switch key { case "progress": return true default: return super.needsDisplay(forKey: key) } } override func display() { let progress = presentation()?.progress ?? self.progress os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress) UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0) guard let context = UIGraphicsGetCurrentContext() else { assertionFailure() return } context.clear(bounds) var rect = bounds let newWidth = CGFloat(progress) * rect.width let widthChanged = rect.width - newWidth rect.size.width = newWidth switch UIApplication.shared.userInterfaceLayoutDirection { case .rightToLeft: rect.origin.x += widthChanged default: break } let path = UIBezierPath(rect: rect) context.setFillColor(tintColor.cgColor) context.addPath(path.cgPath) context.fillPath() contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage UIGraphicsEndImageContext() } } final class StripProgressView: UIView { var disposeBag = Set() private let stripProgressLayer: StripProgressLayer = { let layer = StripProgressLayer() return layer }() override var tintColor: UIColor! { didSet { stripProgressLayer.tintColor = tintColor setNeedsDisplay() } } func setProgress(_ progress: CGFloat, animated: Bool) { stripProgressLayer.removeAnimation(forKey: "progressAnimationKey") if animated { let animation = CABasicAnimation(keyPath: "progress") animation.fromValue = stripProgressLayer.presentation()?.progress ?? stripProgressLayer.progress animation.toValue = progress animation.duration = 0.33 animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) animation.isRemovedOnCompletion = true stripProgressLayer.add(animation, forKey: "progressAnimationKey") stripProgressLayer.progress = progress } else { stripProgressLayer.progress = progress stripProgressLayer.setNeedsDisplay() } } override init(frame: CGRect) { super.init(frame: frame) _init() } required init?(coder: NSCoder) { super.init(coder: coder) _init() } } extension StripProgressView { private func _init() { layer.addSublayer(stripProgressLayer) updateLayerPath() } override func layoutSubviews() { super.layoutSubviews() updateLayerPath() } } extension StripProgressView { private func updateLayerPath() { guard bounds != .zero else { return } stripProgressLayer.frame = bounds stripProgressLayer.tintColor = tintColor stripProgressLayer.setNeedsDisplay() } } #if DEBUG import SwiftUI struct VoteProgressStripView_Previews: PreviewProvider { static var previews: some View { Group { UIViewPreview() { StripProgressView() } .frame(width: 100, height: 44) .padding() .background(Color.black) .previewLayout(.sizeThatFits) UIViewPreview() { let bar = StripProgressView() bar.tintColor = .white bar.setProgress(0.5, animated: false) return bar } .frame(width: 100, height: 44) .padding() .background(Color.black) .previewLayout(.sizeThatFits) UIViewPreview() { let bar = StripProgressView() bar.tintColor = .white bar.setProgress(1.0, animated: false) return bar } .frame(width: 100, height: 44) .padding() .background(Color.black) .previewLayout(.sizeThatFits) } } } #endif