Как установить CADisplayLink в Swift со слабой ссылкой между целью и экземпляром CADisplayLink

В Objective-C мы можем запустить CADisplayLink с шаблоном прокси, чтобы разрушить сильную ссылку:

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

Затем просто аннулируйте displayLink в dealloc:

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

Однако NSProxy, похоже, не может быть унаследован в Swift: https://bugs.swift.org/browse/SR-1715

Я пробовал писать вот так:

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

Это не сработало.

Я хотел бы знать, есть ли способ добиться этого, как в Objective-C.


person a_tuo    schedule 21.05.2017    source источник


Ответы (3)


Лучшим подходом может быть аннулирование отображаемой ссылки в viewWill/DidDisappear, см. Также

за полезной информацией.

Если это не вариант: Сделайте прокси-объект наследовать от NSObject вместо NSProxy. Решение Objective-C, например, приведено здесь

и это можно легко перевести на 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
    }
}

который затем можно использовать как

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

Ваш подход

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

не работает, потому что он разворачивает weakSelf при инициализации CADisplayLink и передает сильную ссылку на self в качестве цели.

person Martin R    schedule 21.05.2017
comment
Это вызовет ошибку unrecognized selector, если вы попытаетесь вызвать селектор после освобождения target. - person Caleb Friden; 21.09.2019

Этот прокси-класс должен просто работать. Не забудьте сделать недействительным перед освобождением.

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
    }
}

Использование:

class ViewController: UIViewController {

    var displaylinkProxy: CADisplayLinkProxy?

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

    @objc func updateAnything() {
        print(Date())
    }
}
person rockdaswift    schedule 15.02.2018
comment
Большое спасибо, @rockdaswift! Но вы должны изменить одну вещь - добавить lazy перед var displayLinkProxy, потому что не сможет поймать себя (я имею в виду класс). - person Alex Kolovatov; 07.05.2020
comment
Привет, @AlexKolovatov, я обновил блок использования более простым для понимания примером, мне не удалось заставить его работать с ключевым словом lazy. - person rockdaswift; 08.05.2020

Другое решение, оно скрывает среду выполнения proxy / objc от внешнего API. DisplayLink остается активным до тех пор, пока на него ссылается переменная. Как только переменная выходит за пределы области видимости или устанавливается равной нулю, CADisplayLink становится недействительным, так что цель также может быть деинициализирована.

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)
    }
}
person Berik    schedule 05.06.2020