How to Set Cadisplaylink in Swift with Weak Reference Between Target and Cadisplaylink Instance

How to set CADisplayLink in Swift with weak reference between target and CADisplayLink instance

An better approach might be to invalidate the display link in
viewWill/DidDisappear, see also

  • Correct handling / cleanup / etc of CADisplayLink in Swift custom animation?

for useful information.

If that is not an option: Make the proxy object inherit from NSObject
instead of NSProxy. An Objective-C solution is for example
given here

  • CADisplayLink at iOS 6.0 not retaining target

and that can easily be translated to Swift 3:

class JAWeakProxy: NSObject {
weak var target: NSObjectProtocol?

init(target: NSObjectProtocol) {
self.target = target
super.init()
}

override func responds(to aSelector: Selector!) -> Bool {
return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
}

override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
}

which can then be used as

displayLink = CADisplayLink(target: JAWeakProxy(target: self),
selector: #selector(didRefresh(dpLink:)))

Your approach

weak var weakSelf = self    
displayLink = CADisplayLink(target: weakSelf!, selector: #selector(displayDidRefresh(dpLink:)))

does not work because it unwraps weakSelf when the CADisplayLink
is initialized and passes a strong reference to self as the target.

Correct handling / cleanup / etc of CADisplayLink in Swift custom animation?

Here’s a simple example showing how I’d go about implementing a CADisplayLink (in Swift 5):

class C { /// your view class or whatever

private var displayLink: CADisplayLink?
private var startTime = 0.0
private let animationLength = 5.0

func startDisplayLink() {

stopDisplayLink() /// make sure to stop a previous running display link
startTime = CACurrentMediaTime() // reset start time

/// create displayLink and add it to the run-loop
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}

@objc func displayLinkDidFire(_ displayLink: CADisplayLink) {

var elapsedTime = CACurrentMediaTime() - startTime

if elapsedTime > animationLength {
stopDisplayLink()
elapsedTime = animationLength /// clamp the elapsed time to the animation length
}

/// do your animation logic here
}

/// invalidate display link if it's non-nil, then set to nil
func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
}

Points to note:

  • We’re using nil here to represent the state in which the display link isn’t running – as there’s no easy way of getting this information from an invalidated display link.
  • Instead of using removeFromRunLoop(), we’re using invalidate(), which will not crash if the display link hasn’t already been added to a run-loop. However this situation should never arise in the first place – as we’re always immediately adding the display link to the run-loop after creating it.
  • We’ve made the displayLink private in order to prevent outside classes from putting it in an unexpected state (e.g invalidating it but not setting it to nil).
  • We have a single stopDisplayLink() method that both invalidates the display link (if it is non-nil) and sets it to nil – rather than copy and pasting this logic.
  • We’re not setting paused to true before invalidating the display link, as this is redundant.
  • Instead of force unwrapping the displayLink after checking for non-nil, we’re using optional chaining e.g displayLink?.invalidate() (which will call invalidate() if the display link isn’t nil). While force unwrapping may be ‘safe’ in your given situation (as you’re checking for nil) – it’s potentially unsafe when it comes to future refactoring, as you may re-structure your logic without considering what impact this has on the force unwraps.
  • We’re clamping the elapsed time to the animation duration in order to ensure that the later animation logic doesn’t produce a value out of the expected range.
  • Our update method displayLinkDidFire(_:) takes a single argument of type CADisplayLink, as required by the documentation.

Using @EnvironmentObject properties with CADisplayLink

Just remove @EnvironmentObject property wrapper, it is for SwiftUI only

class MyAnimations: NSObject{
var mainData: MainData

init(mainData: MainData) {
self.mainData = mainData
super.init()
}

// ... other code
}

CADisplayLink target selector being triggered after it is invalidated

This might be a bit late but since there has been no answers...

I do not think, your selector is called once more, but rather the display link's thread is in the middle of your draw frame method. In any case, the problem is quite the same.. This is multithreading and by trying to dealloc some objects in one thread while using them in another will usually result in a conflict.

Probably the easiest solution would be putting a flag and an "if statement" in your draw frame method as

if(schaduledForDestruction) {
[self destroy];
return;
}

and then wherever you are invalidating your display link set "schaduledForDestruction" to YES.

If you really think the display link calls tis method again, you could use another if inside that one "destructionInProgress".

If you do not want to change the draw frame method, you could try forcing a new selector to the display link...

CADisplayLink *myDisplayLink;
BOOL resourcesLoaded;
SEL drawSelector;

- (void)destroy {
if(resourcesLoaded) {
[myDisplayLink invalidate];
//free resources
resourcesLoaded = NO;
}
}
- (void)metaLevelDraw {
[self performSelector:drawSelector];
}
- (void)drawFrame {
//draw stuff
}
- (void)beginAnimationing {
drawSelector = @selector(drawFrame);
myDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(metaLevelDraw)];
[myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)endAnimationing {
drawSelector = @selector(destroy);
}

or just consider something like this (but I can't say this is safe. If the newly created display link can run the selector on a different thread then the original, it solves nothing)..

CADisplayLink *myDisplayLink;
BOOL resourcesLoaded;

- (void)destroy {
if(resourcesLoaded) {
[myDisplayLink invalidate];
//free resources
resourcesLoaded = NO;
}
}
- (void)drawFrame {
//draw stuff
}
- (void)beginAnimationing {
myDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)endAnimationing {
[myDisplayLink invalidate];
myDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(destroy)];
[myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

Definitively, do you have to invalidate() a CADisplayLink when the controller disappears?

A run loop keeps strong references to any display links that are added to it. See add(to:forMode:) documentation:

The run loop retains the display link. To remove the display link from all run loops, send an invalidate() message to the display link.

And a display link keeps strong reference to its target. See invalidate() documentation:

Removing the display link from all run loop modes causes it to be released by the run loop. The display link also releases the target.

So, you definitely have to invalidate(). And if you're using self as the target of the display link, you cannot do this in deinit (because the CADisplayLink keeps a strong reference to its target).


A common pattern if doing this within a view controller is to set up the display link in viewDidAppear and remove it in viewDidDisappear.

For example:

private weak var displayLink: CADisplayLink?

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startDisplayLink()
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopDisplayLink()
}

private func startDisplayLink() {
stopDisplayLink() // stop previous display link if one happens to be running

let link = CADisplayLink(target: self, selector: #selector(handle(displayLink:)))
link.add(to: .main, forMode: .commonModes)
displayLink = link
}

private func stopDisplayLink() {
displayLink?.invalidate()
}

@objc func handle(displayLink: CADisplayLink) {
// do something
}

CADisplayLink Timer Not working proper | Swift

If you want to create a display link, you would generally just call CADisplayLink(target:selector:). See CADisplayLink documentation suggests that it would be something like:

weak var displayLink: CADisplayLink?

func createDisplayLink() {
self.displayLink?.invalidate() // cancel prior one, if any

let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}

@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
print(displayLink.timestamp)
}

(So, there’s no need to navigate up from the view to the window to the screen. Just create your display link and add it to the main run loop. And if you’re going to save a reference to it, I’d call it a displayLink, not timer, to avoid confusion. Also, I’ve give that handler a name and parameter that makes its purpose self-evident.)

But let’s set that aside. The question is whether you need/want to use a display link at all. Display links are for timers that must be optimally tied to the screen refresh rate (e.g. it’s for timers that update the UI, e.g. animations, stopwatch-like text fields, etc.).

That’s inefficient, especially doing it every cell. You’re firing off a separate display link for every cell, 60 times per second. If you had 20 cells visible, then your method would be called 1,200 times per second. Instead, you probably just one call per cell every three seconds. E.g., if you want to know if a cell has been shown for 3 seconds, you might just:

  • create a non-repeating, three-second Timer when the cell is displayed (e.g. willDisplay);
  • invalidate the Timer when the cell is no longer shown (e.g. in didEndDisplaying), and
  • if the timer handler fires, that means the cell was shown for three seconds.

But it’s a single timer event after 3 seconds, not calling it 60 times per second per cell.



Related Topics



Leave a reply



Submit