How to set CADisplayLink in Swift with weak reference between target and CADisplayLink instance
An better approach might be to invalidate the display link inviewWill/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 usinginvalidate()
, 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 tonil
). - We have a single
stopDisplayLink()
method that both invalidates the display link (if it is non-nil) and sets it tonil
– rather than copy and pasting this logic. - We’re not setting
paused
totrue
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.gdisplayLink?.invalidate()
(which will callinvalidate()
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 typeCADisplayLink
, 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
theTimer
when the cell is no longer shown (e.g. indidEndDisplaying
), 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
Image Disappears When Styling Class to Make a Round Image
How to Make Exponents in the Swiftui
My Button Is Centered for iPhone 6 and 6 Plus, But Not for iPhone 5
Downloading Firebase Storage Files Device Issue
How to Pass Arguments into a Function with Completion Swift
Spritekiit Swift: Touch a Moving Object
Swift Updating Screen in Between Steps of While Loop
How Assistant Model Works in Swift
(Cross-)Compiling Swift for Raspberry Pi
How to Set Cadisplaylink in Swift with Weak Reference Between Target and Cadisplaylink Instance
Swift Generic Protocol Function Parameters
Why Does Classa Adopting Protocolb Not Satisfy the Protocolb Requirement
Accessing and Manipulating Array Item in an Environmentobject
What Are 3 Items in a Swift Method Parameter For
How to Deallocate All References Elements from an Array