During development, we sometimes encounter the need to periodically redraw UIView to animate it differently.

In this paper,project

rendering

A preliminary CADisplayLink

However, the implementation of such animation is not smooth, because there are many different inputs to be processed in the runloop where the timer is. As a result, the minimum period of the timer is between 50 and 100 milliseconds, and it can only run about 20 times in one second at most.

But if we want to see smooth animation on the screen, we need to maintain a refresh rate of 60 frames, which means that each frame has to be about 0.016 seconds apart, which is not possible with NSTimer. So we’re going to use another timer from Core Animation, CADisplayLink.

In CADisplayLink’s header file, we can see that it is used in a very similar way to NSTimer. It also needs to be registered with RunLoop, but unlike NSTimer, It makes RunLoop call the selector specified by CADisplayLink when the screen needs to be redrawn to prepare the data for the next frame to display. NSTimer is a selector that doesn’t call until the last RunLoop is complete, so it calls much more frequently than NSTimer.

Also, unlike NSTimer, NSTimer can specify timeInterval, which corresponds to the interval between selector calls, but if NSTimer’s trigger time is up and RunLoop is blocked, its trigger time will be postponed to the next RunLoop. CADisplayLink’s timer interval cannot be adjusted, which is fixed at 60 times per second, but you can set the number of frames between calls to a Selector by setting its frameInterval property. Note also that if the selector executes code that exceeds the duration of the frameInterval, CADisplayLink will simply ignore that frame and run it again on the next update.

Configuration RunLoop

When creating CADisplayLink, we need to specify a RunLoop and RunLoopMode. Usually we choose to use the main RunLoop, because all UI updates must be done in the main thread. In the mode selection, NSDefaultRunLoopMode can be used, but the smooth operation of animation cannot be guaranteed, so NSRunLoopCommonModes can be used instead. Be careful, though, because if the animation runs at a high frame rate, it will cause some other timer like task or other iOS animation like slide to pause until the animation ends.

private func setup(a) {
	_displayLink = CADisplayLink(target: self, selector: #selector(update)) _displayLink? .isPaused =true_displayLink? .add(to:RunLoop.main, forMode: .commonModes)
}

Copy the code

Realize different character transformation animation

With the CADisplayLink timer successfully set up, you are ready to animate the string. In this case, we’re going to use NSAttributedString for the effect

In setupAnimatedText (from labelText: String? In this method, we need to use two arrays, one is durationArray, the other is delayArray. By configuring the values in these two arrays, we can control the occurrence time and duration of each character in the string.

Typewriter effect configuration

  • Each character takes the same amount of time to appear
  • The next character appears after the previous character has completed
  • By modifying theNSAttributedStringKey.baselineOffsetAdjust theCharacter position
case .typewriter:
	attributedString.addAttribute(.baselineOffset, value: -label.font.lineHeight, range: NSRange(location: 0, length: attributedString.length))
	let displayInterval = duration / TimeInterval(attributedString.length)
	for index in 0..<attributedString.length {
		durationArray.append(displayInterval)
		delayArray.append(TimeInterval(index) * displayInterval)
	}

Copy the code

Flicker effect configuration

  • The time required for each character to appear is random
  • Make sure all characters can be indurationInside all complete appear
  • Modify theNSAttributedStringKey.foregroundColorthetransparencyTo achieve the appearance of the character effect
case .shine:
	attributedString.addAttribute(.foregroundColor, value: label.textColor.withAlphaComponent(0), range: NSRange(location: 0, length: attributedString.length))
	for index in 0..<attributedString.length {
		delayArray.append(TimeInterval(arc4random_uniform(UInt32(duration) / 2 * 100) / 100))
		let remain = duration - Double(delayArray[index])
		durationArray.append(TimeInterval(arc4random_uniform(UInt32(remain) * 100) / 100))}Copy the code

The configuration of the fade effect

  • The time required for each character to appear decreases
  • Modify theNSAttributedStringKey.foregroundColorthetransparencyTo achieve the appearance of the character effect
case .fade:
	attributedString.addAttribute(.foregroundColor, value: label.textColor.withAlphaComponent(0), range: NSRange(location: 0, length: attributedString.length))
	let displayInterval = duration / TimeInterval(attributedString.length)
	for index in 0..<attributedString.length  {
		delayArray.append(TimeInterval(index) * displayInterval)
		durationArray.append(duration - delayArray[index])
	}
Copy the code

Improved string update effect per frame

Next we need to refine the update method we configured in CADisplayLink, where we transform strings based on the data in the two arrays we just configured.

The core code

  • Get animation progress by start time and current time
  • According to theCharacter positionThe correspondingduationArraywithdelayArrayThe data in the
  • According to thedurationArraywithdelayArrayIs used to calculate the current characterAccording to schedule
var percent = (CGFloat(currentTime - beginTime) - CGFloat(delayArray[index])) / CGFloat(durationArray[index])
percent = fmax(0.0, percent)
percent = fmin(1.0, percent)
attributedString.addAttribute(.baselineOffset, value: (percent - 1) * label! .font.lineHeight, range: range)Copy the code

The processed NSAttributedString can then be returned to the label for updating

Outside: use sine function to realize ripple progress

Corrugated path

Let’s start with the sine function: y = A sine (ax + b)

  • We’re going to shift it by b in the x direction.
  • The abscissa extends (0 < a < 1) or shortens (A > 1) by 1/a times
  • Elongate (A > 1) or shorten (0 < A < 1) A times

With that in mind, let’s go back to the wavePath() method, where we use sine functions to draw a UIBezierPath:

let originY = (label.bounds.size.height + label.font.lineHeight) / 2
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: _waveHeight!) )var yPosition = 0.0
for xPosition in 0..<Int(label.bounds.size.width) {
	yPosition = _zoom! * sin(Double(xPosition) / 180.0 * Double.pi - 4 * _translate! / Double.pi) * 5 + _waveHeight!
	path.addLine(to: CGPoint(x: Double(xPosition), y: yPosition))
}
path.addLine(to: CGPoint(x: label.bounds.size.width, y: originY))
path.addLine(to: CGPoint(x: 0, y: originY))
path.addLine(to: CGPoint(x: 0, y: _waveHeight!) ) path.close()Copy the code

Update ripple height with animation

  • The height increases as you progress
  • The ripple fluctuates with progress

In the update method registered with CADisplayLink, we update the Layer that carries the ripple path

_waveHeight! -= duration / Double(label! .font.lineHeight) _translate! + =0.1
if! _reverse { _zoom! + =0.02
	if_zoom! > =1.2 {
		_reverse = true}}else{ _zoom! - =0.02
	if_zoom! < =1.0 {
		_reverse = false
	}
}
shapeLayer.path = wavePath()
Copy the code

conclusion

These are some of the ways I use CADisplayLink. In fact, there are many ways to use CADisplayLink. You can use CADisplayLink to achieve more complex and beautiful animations.

If you like this project, feel free to give me a star on GitHub.

reference

  • RQShineLabel
  • Apple Developer Document – CADisplayLink
  • IOS core animation advanced skills