import Foundation import UIKit import AsyncDisplayKit import Display private func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0, lineWidth: CGFloat = 2.0) -> UIImage? { return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setStrokeColor(color.cgColor) context.setLineWidth(lineWidth) context.setLineCap(.round) let cutoutAngle: CGFloat = CGFloat.pi * 30.0 / 180.0 context.addArc(center: CGPoint(x: size.width / 2.0, y: size.height / 2.0), radius: size.width / 2.0 - lineWidth / 2.0, startAngle: 0.0, endAngle: CGFloat.pi * 2.0 - cutoutAngle, clockwise: false) context.strokePath() }) } private func convertIndicatorColor(_ color: UIColor) -> UIColor { if color.isEqual(UIColor(rgb: 0x007aff)) { return .gray } else if color.isEqual(UIColor(rgb: 0x2ea6ff)) { return .white } else if color.isEqual(UIColor(rgb: 0x000000)) || color.isEqual(UIColor.black) { return .gray } else { return color } } public enum ActivityIndicatorType: Equatable { case navigationAccent(UIColor) case custom(UIColor, CGFloat, CGFloat, Bool) public static func ==(lhs: ActivityIndicatorType, rhs: ActivityIndicatorType) -> Bool { switch lhs { case let .navigationAccent(lhsColor): if case let .navigationAccent(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { return true } else { return false } case let .custom(lhsColor, lhsDiameter, lhsWidth, lhsForceCustom): if case let .custom(rhsColor, rhsDiameter, rhsWidth, rhsForceCustom) = rhs, lhsColor.isEqual(rhsColor), lhsDiameter == rhsDiameter, lhsWidth == rhsWidth, lhsForceCustom == rhsForceCustom { return true } else { return false } } } } public enum ActivityIndicatorSpeed { case regular case slow } public final class ActivityIndicator: ASDisplayNode { public var type: ActivityIndicatorType { didSet { switch self.type { case let .navigationAccent(color): self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color) case let .custom(color, diameter, lineWidth, _): self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter, lineWidth: lineWidth) } switch self.type { case let .navigationAccent(color): self.indicatorView?.color = color case let .custom(color, _, _, _): self.indicatorView?.color = convertIndicatorColor(color) } } } private var currentInHierarchy = false override public var isHidden: Bool { didSet { self.updateAnimation() } } private let speed: ActivityIndicatorSpeed private let indicatorNode: ASImageNode private var indicatorView: UIActivityIndicatorView? public init(type: ActivityIndicatorType, speed: ActivityIndicatorSpeed = .regular) { self.type = type self.speed = speed self.indicatorNode = ASImageNode() self.indicatorNode.isLayerBacked = true self.indicatorNode.displaysAsynchronously = false super.init() if case let .custom(_, _, _, forceCustom) = self.type, forceCustom { //self.isLayerBacked = true } switch type { case let .navigationAccent(color): self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color) case let .custom(color, diameter, lineWidth, forceCustom): self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter, lineWidth: lineWidth) if forceCustom { self.addSubnode(self.indicatorNode) } } } override public func didLoad() { super.didLoad() let indicatorView: UIActivityIndicatorView switch self.type { case let .navigationAccent(color): indicatorView = UIActivityIndicatorView(style: .whiteLarge) indicatorView.color = color case let .custom(color, diameter, _, forceCustom): indicatorView = UIActivityIndicatorView(style: diameter < 15.0 ? .white : .whiteLarge) indicatorView.color = convertIndicatorColor(color) if !forceCustom { self.view.addSubview(indicatorView) } } self.indicatorView = indicatorView let size = self.bounds.size if !size.width.isZero { self.layoutContents(size: size) } } private var isAnimating = false { didSet { if self.isAnimating != oldValue { if self.isAnimating { self.indicatorView?.startAnimating() let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) switch self.speed { case .regular: basicAnimation.duration = 0.5 case .slow: basicAnimation.duration = 0.7 } basicAnimation.fromValue = NSNumber(value: Float(0.0)) basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) basicAnimation.repeatCount = Float.infinity basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) basicAnimation.beginTime = 1.0 self.indicatorNode.layer.add(basicAnimation, forKey: "progressRotation") } else { self.indicatorView?.stopAnimating() self.indicatorNode.layer.removeAnimation(forKey: "progressRotation") } } } } private func updateAnimation() { self.isAnimating = !self.isHidden && self.currentInHierarchy } override public func willEnterHierarchy() { super.willEnterHierarchy() self.currentInHierarchy = true self.updateAnimation() } override public func didExitHierarchy() { super.didExitHierarchy() self.currentInHierarchy = false self.updateAnimation() } override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { switch self.type { case .navigationAccent: return CGSize(width: 22.0, height: 22.0) case let .custom(_, diameter, _, _): return CGSize(width: diameter, height: diameter) } } override public func layout() { super.layout() let size = self.bounds.size self.layoutContents(size: size) } private func layoutContents(size: CGSize) { let indicatorSize: CGSize let shouldScale: Bool switch self.type { case .navigationAccent: indicatorSize = CGSize(width: 22.0, height: 22.0) shouldScale = false case let .custom(_, diameter, _, forceDefault): indicatorSize = CGSize(width: diameter, height: diameter) shouldScale = !forceDefault } self.indicatorNode.frame = CGRect(origin: CGPoint(x: ((size.width - indicatorSize.width) / 2.0), y: ((size.height - indicatorSize.height) / 2.0)), size: indicatorSize) if shouldScale, let indicatorView = self.indicatorView { let intrinsicSize = indicatorView.bounds.size self.subnodeTransform = CATransform3DMakeScale(min(1.0, indicatorSize.width / intrinsicSize.width), min(1.0, indicatorSize.height / intrinsicSize.height), 1.0) indicatorView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) } } }