我有一个Timer(),要求视图每隔x秒刷新一次:
func updateTimer(_ stopwatch: Stopwatch) {
stopwatch.counter = stopwatch.counter + 0.035
Stopwatch.circlePercentage += 0.035
stopwatchViewOutlet.setNeedsDisplay()
}
-
class StopwatchView: UIView, UIGestureRecognizerDelegate {
let buttonClick = UITapGestureRecognizer()
override func draw(_ rect: CGRect) {
Stopwatch.drawStopwatchFor(view: self, gestureRecognizers: buttonClick)
}
}
-
extension Stopwatch {
static func drawStopwatchFor(view: UIView, gestureRecognizers: UITapGestureRecognizer) {
let scale: CGFloat = 0.8
let joltButtonView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 75, height: 75))
let imageView: UIImageView!
// -- Need to hook this to the actual timer --
let temporaryVariableForTimeIncrement = CGFloat(circlePercentage)
// Exterior of sphere:
let timerRadius = min(view.bounds.size.width, view.bounds.size.height) / 2 * scale
let timerCenter = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let path = UIBezierPath(arcCenter: timerCenter, radius: timerRadius, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
path.lineWidth = 2.0
UIColor.blue.setStroke()
path.stroke()
// Interior of sphere
let startAngle = -CGFloat.pi / 2
let arc = CGFloat.pi * 2 * temporaryVariableForTimeIncrement / 100
let cPath = UIBezierPath()
cPath.move(to: timerCenter)
cPath.addLine(to: CGPoint(x: timerCenter.x + timerRadius * cos(startAngle), y: timerCenter.y))
cPath.addArc(withCenter: timerCenter, radius: timerRadius * CGFloat(0.99), startAngle: startAngle, endAngle: arc + startAngle, clockwise: true)
cPath.addLine(to: CGPoint(x: timerCenter.x, y: timerCenter.y))
let circleShape = CAShapeLayer()
circleShape.path = cPath.cgPath
circleShape.fillColor = UIColor.cyan.cgColor
view.layer.addSublayer(circleShape)
// Jolt button
joltButtonView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
joltButtonView.layer.cornerRadius = 38
joltButtonView.backgroundColor = UIColor.white
imageView = UIImageView(frame: joltButtonView.frame)
imageView.contentMode = .scaleAspectFit
imageView.image = image
joltButtonView.addGestureRecognizer(gestureRecognizers)
view.addSubview(joltButtonView)
view.addSubview(imageView)
}
}
我的第一个问题是每次都会重新绘制子视图。第二个问题是,经过几秒钟后,性能开始恶化。
Too many instances of the subview
Trying to drive the blue graphic with a Timer()
我是否应该尝试设置圆形百分比图形的动画,而不是每次调用计时器功能时重绘它?
答案 0 :(得分:2)
主要观察结果是您不应在draw
中添加子视图。这用于渲染单个帧,您不应该在draw
内的视图层次结构中添加/删除内容。因为您大约会在0.035秒内添加子视图,所以您的视图层次结构将会爆炸,会对内存和性能产生不利影响。
您应该使用draw
方法仅在更新的stroke
上为秒针调用UIBezierPath
。或者,如果使用CAShapeLayer
,只需更新其path
(根本不需要draw
方法。)
所以,几个外围观察:
您正在为计时器的每个刻度增加“百分比”。但是你不能保证你的计时器被调用的频率。因此,不是增加一些“百分比”,而是应该在启动计时器时保存开始时间,然后在计时器的每个滴答时,你应该重新计算从那时到现在经过多长时间的时间。过去。
您正在使用Timer
这对大多数用途都有好处,但为了获得最佳绘图效果,您真的应该使用CADisplayLink
,这是最佳定时允许您执行的操作在下次刷新屏幕之前你需要的任何东西。
所以,这是一个简单的钟面的示例,其中有一个全面的秒针:
open class StopWatchView: UIView {
let faceLineWidth: CGFloat = 5 // LineWidth of the face
private var startTime: Date? { didSet { self.updateHand() } } // When was the stopwatch resumed
private var oldElapsed: TimeInterval? { didSet { self.updateHand() } } // How much time when stopwatch paused
public var elapsed: TimeInterval { // How much total time on stopwatch
guard let startTime = startTime else { return oldElapsed ?? 0 }
return Date().timeIntervalSince(startTime) + (oldElapsed ?? 0)
}
private weak var displayLink: CADisplayLink? // Display link animating second hand
public var isRunning: Bool { return displayLink != nil } // Is the timer running?
private var clockCenter: CGPoint { // Center of the clock face
return CGPoint(x: bounds.midX, y: bounds.midY)
}
private var radius: CGFloat { // Radius of the clock face
return (min(bounds.width, bounds.height) - faceLineWidth) / 2
}
private lazy var face: CAShapeLayer = { // Shape layer for clock face
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = self.faceLineWidth
shapeLayer.strokeColor = #colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1).cgColor
shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
return shapeLayer
}()
private lazy var hand: CAShapeLayer = { // Shape layer for second hand
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 3
shapeLayer.strokeColor = #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1).cgColor
shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
return shapeLayer
}()
override public init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
// add necessary shape layers to layer hierarchy
private func configure() {
layer.addSublayer(face)
layer.addSublayer(hand)
}
// if laying out subviews, make sure to resize face and update hand
override open func layoutSubviews() {
super.layoutSubviews()
updateFace()
updateHand()
}
// stop display link when view disappears
//
// this prevents display link from keeping strong reference to view after view is removed
override open func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
pause()
}
}
// MARK: - DisplayLink routines
/// Start display link
open func resume() {
// cancel any existing display link, if any
pause()
// start new display link
startTime = Date()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .commonModes)
// save reference to it
self.displayLink = displayLink
}
/// Stop display link
open func pause() {
displayLink?.invalidate()
// calculate floating point number of seconds
oldElapsed = elapsed
startTime = nil
}
open func reset() {
pause()
oldElapsed = nil
}
/// Display link handler
///
/// Will update path of second hand.
@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
updateHand()
}
/// Update path of clock face
private func updateFace() {
face.path = UIBezierPath(arcCenter: clockCenter, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
/// Update path of second hand
private func updateHand() {
// convert seconds to an angle (in radians) and figure out end point of seconds hand on the basis of that
let angle = CGFloat(elapsed / 60 * 2 * .pi - .pi / 2)
let endPoint = CGPoint(x: clockCenter.x + cos(angle) * radius * 0.9, y: clockCenter.y + sin(angle) * radius * 0.9)
// update path of hand
let path = UIBezierPath()
path.move(to: clockCenter)
path.addLine(to: endPoint)
hand.path = path.cgPath
}
}
和
class ViewController: UIViewController {
@IBOutlet weak var stopWatchView: StopWatchView!
@IBAction func didTapStartStopButton () {
if stopWatchView.isRunning {
stopWatchView.pause()
} else {
stopWatchView.resume()
}
}
@IBAction func didTapResetButton () {
stopWatchView.reset()
}
}
现在,在上面的示例中,我只是更新CAShapeLayer
处理程序中秒针的CADisplayLink
。就像我在开始时所说的那样,您也可以使用draw
方法简单地描绘单帧动画所需的路径,然后在显示链接处理程序中调用setNeedsDisplay
。但是,如果您这样做,请不要更改draw
中的视图层次结构,而是在init
和draw
中执行您需要的任何配置stroke
无论路径应该是什么就在那一刻。