Using @objc in Swift 5 protocols

Viewed 320

I have an issue with using @objc code in swift protocols and was wondering if there is a workaround for that.

Currently, I have code:

import UIKit

@objc
protocol TrackScreenshot {
    func registerObserver()
    func removeObservers()
}

extension TrackScreenshot where Self: ScreenTracking {
    func registerObserver() {
        NotificationCenter.default.addObserver(self, selector: #selector(trackScreenshot), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
    }
    
    func removeObservers() {
        NotificationCenter.default.removeObserver(self, name: UIApplication.userDidTakeScreenshotNotification, object: nil )
    }
    
    func trackScreenshot() {
        print(screenName.rawValue)
    }
}

So I want to inherit the TrackScreenshot protocol and make screens easily trackable. BUT there is an issue. registerObserver() method on #selecor asks to add @objc to trackScreenshot method, but if I do so, Xcode complains on trackScreenshot() line and telling: @objc can only be used with members of classes, @objc protocols, and concrete extensions of classes

Is there a way to fix this? Also tried:

    NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: nil) { _ in
        print(self.screenName.rawValue)
    }

but it's not working, and the observer can't be removed and remains in the circle, so prints all the previous screen names when a new screen is opened.

Any help is more then welcome! Thanks in advance!

2 Answers

I would use the closure form of notification observation rather than a selector/method:

protocol TrackScreenshot {
    func registerObserver(handler: (()->Void)?)
    func removeObservers()
}

extension TrackScreenshot where Self: ScreenTracking {
    func registerObserver(handler: (()->Void)?) {
        NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: nil) { (notification) in
            handler?()
        }
    }
    
    func removeObservers() {
        NotificationCenter.default.removeObserver(self, name: UIApplication.userDidTakeScreenshotNotification, object: nil )
    }
}

Then your use is something like:

self.registerObserver { [weak self] in
    guard let self = self else {
        return
    }
    print("Screen shot")'
}

You can track screenshot notification using delegates too, feel free to refactor as per your need:

public protocol ScreenshotDelegate: AnyObject {
    func screenshotDetected()
}

open class ScreenshotTracker: NSObject {
    
    private weak var delegate: ScreenshotDelegate?
    
    public init(delegate: ScreenshotDelegate) {
        self.delegate = delegate
       
        NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: OperationQueue.main) { notification in
            delegate.screenshotDetected()
            print("Screenshot notification")
        }
    }
}

ViewController Setup:

override func viewDidLoad() {
    super.viewDidLoad()
    let _ = ScreenshotTracker(delegate: self)
}

extension ViewController: ScreenshotDelegate {
    func screenshotDetected() {
        print("screenshot taken!!!")
    }
}
Related