2

In Objective-C, we can init CADisplayLink with Proxy Pattern to break strong reference:

WeakProxy *weakProxy = [WeakProxy weakProxyForObject:self];
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];

Then, just invalidate the displayLink in dealloc:

- (void)dealloc
{
    [_displayLink invalidate];
}

However, NSProxy seems can't be inherited in Swift: https://bugs.swift.org/browse/SR-1715

I tried to write like this:

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

It didn't work.

I would like to know if there is any way to achieve this like in Objective-C.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
a_tuo
  • 651
  • 7
  • 23

3 Answers3

4

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

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

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.

Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • This will throw an `unrecognized selector` error if you attempt to call the selector after the `target` has been deallocated. – Caleb Friden Sep 20 '19 at 22:10
3

This proxy class should just work. Don't forget to invalidate before the dealloc.

import UIKit

class CADisplayLinkProxy {

    var displaylink: CADisplayLink?
    var handle: (() -> Void)?

    init(handle: (() -> Void)?) {
        self.handle = handle
        displaylink = CADisplayLink(target: self, selector: #selector(updateHandle))
        displaylink?.add(to: RunLoop.current, forMode: .common)
    }

    @objc func updateHandle() {
        handle?()
    }

    func invalidate() {
        displaylink?.remove(from: RunLoop.current, forMode: .common)
        displaylink?.invalidate()
        displaylink = nil
    }
}

Usage:

class ViewController: UIViewController {

    var displaylinkProxy: CADisplayLinkProxy?

    override func viewDidLoad() {
        super.viewDidLoad()
        displaylinkProxy = CADisplayLinkProxy(handle: { [weak self] in
            self?.updateAnything()
        })
    }

    @objc func updateAnything() {
        print(Date())
    }
}
rockdaswift
  • 9,613
  • 5
  • 40
  • 46
  • Thanks a lot, @rockdaswift! But you should change one thing - add 'lazy' before var displayLinkProxy, because won't be able to catch self (I mean class). – Alex Kolovatov May 07 '20 at 16:42
  • 1
    Hi @AlexKolovatov, I have updated the usage block with an easier to understand example, I wasn't able to make it work with the lazy keyword. – rockdaswift May 08 '20 at 07:11
1

Another solution, this one hides the proxy / objc runtime from its external API. The DisplayLink stays alive for as long as it is referenced by variable. Once the variable goes out of scope or is set to nil, the CADisplayLink is invalidated so that the target can be deinited also.

import Foundation
import UIKit

/// DisplayLink provides a block based interface for CADisplayLink.
/// The CADisplayLink is invalidated upon DisplayLink deinit.
///
/// Usage:
/// ```
/// let displayLink = DisplayLink { caDisplayLink in print("Next frame scheduled \(caDisplayLink.targetTimestamp)") }
/// ```
///
/// Note: Keep a reference to the DisplayLink.
final class DisplayLink {
    let displayLink: CADisplayLink

    init(runloop: RunLoop? = .main, prepareNextFrame: @escaping (CADisplayLink) -> ()) {
        displayLink = CADisplayLink(
            target: DisplayLinkTarget(prepareNextFrame),
            selector: #selector(DisplayLinkTarget.prepareNextFrame))

        if let runloop = runloop {
            displayLink.add(to: runloop, forMode: .default)
        }
    }

    deinit {
        displayLink.invalidate()
    }
}

private class DisplayLinkTarget {
    let callback: (CADisplayLink) -> ()

    init(_ callback: @escaping (CADisplayLink) -> ()) {
        self.callback = callback
    }

    @objc func prepareNextFrame(displaylink: CADisplayLink) {
        callback(displaylink)
    }
}
Berik
  • 7,816
  • 2
  • 32
  • 40