Swift: How to observe if screen is locked in macOS - swift

I want to detect if the user has locked his screen (in macOS) using Swift.
Based on this answer I’ve created the following code:
import Cocoa
import Quartz
if let dict = Quartz.CGSessionCopyCurrentDictionary() as? [String : Any] {
let locked = dict["CGSSessionScreenIsLocked"]
print(locked as? String ?? "")
}
...which seems to work fine if I explicitly run the code.
But how is it possible to observe the value so I get notified when the value got changed?

You can observe distributed notifications. They are not documented.
let dnc = DistributedNotificationCenter.default()
let lockObserver = dnc.addObserver(forName: .init("com.apple.screenIsLocked"),
object: nil, queue: .main) { _ in
NSLog("Screen Locked")
}
let unlockObserver = dnc.addObserver(forName: .init("com.apple.screenIsUnlocked"),
object: nil, queue: .main) { _ in
NSLog("Screen Unlocked")
}

With Combine (available on macOS 10.15+):
import Combine
var bag = Set<AnyCancellable>()
let dnc = DistributedNotificationCenter.default()
dnc.publisher(for: Notification.Name(rawValue: "com.apple.screenIsLocked"))
.sink { _ in print("Screen Locked") }
.store(in: &bag)
dnc.publisher(for: Notification.Name(rawValue: "com.apple.screenIsUnlocked"))
.sink { _ in print("Screen Unlocked") }
.store(in: &bag)

Related

Type of expression is ambiguous without more context in Combine while .store(in: &subscrptions)

// MARK: - Combine
/// First Image
let firstImage = UnsplashAPI.randomeImage()
.flatMap { RandomImageResponse in
ImageDownloader.download(url: RandomImageResponse.urls.regular)
}
/// Second Image
let secondImage = UnsplashAPI.randomeImage()
.flatMap { RandomImageResponse in
ImageDownloader.download(url: RandomImageResponse.urls.regular)
}
firstImage.zip(secondImage)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [unowned self ] completion in
switch completion {
case .finished:
break
case .failure():
self.gameState = .stop
}
},
receiveValue: { [unowned self] first, second in
self.gameImages = [first, second, second, second].shuffled()
self.gameScoreLabel.text = "Score: \(self.gameScore)"
// TODO: Handling game score
self.stopLoaders()
self.setImages()
})
.store(in: &subscrptions)
That's my code:
while using .store(in: &subscrptions)
Here is also --> var subscrptions: Set<AnyCancellable> = []
This is only happening with Xcode 14. Any guesses?
The compiler is probably complaining about the block that begins:
receiveValue: { [unowned self] first, second in
It can't infer the types of first and second. You might be able to resolve the error by indicating their types explicitly. I don't know what type ImageDownloader.download returns, But here's an example using simple types:
import UIKit
import Combine
var greeting = "Hello, playground"
let numbers = [1, 2, 3, 5].publisher
let strings = ["a", "b", "c", "d"].publisher
var cancellables = Set<AnyCancellable>()
numbers.zip(strings)
.sink { completion in
switch completion {
case .failure(let err):
debugPrint(err)
case .finished:
print("Finshed")
}
} receiveValue: { (first:Int, second:String) in
print(first, second)
}
.store(in: &cancellables)
Note how the types are made explicit in the block using (first:Int, second:String) in

Swift: NotificationCenter's addObserver closure keeps getting triggered constantly

I have an AVPlayer to play an MP3 stream. I added an observer for when the stream is ended, the trouble is the closure seems to be stuck in a loop after it's triggered for the first time.
//// Variables
private var periodicTimeObserverToken: Any?
private var finishObserverToken: Any?
//// Setting up the player
self.playerItem = AVPlayerItem(url: filePath)
self.player = AVPlayer(playerItem: self.playerItem)
removePeriodicTimeObserver()
removePlayerFinishedObserver()
addPeriodicTimeObserver()
addPlayerFinishedObserver()
///
private func addPeriodicTimeObserver() {
periodicTimeObserverToken = self.player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main) { [weak self] (CMTime) -> Void in
if self?.player?.currentItem?.status == .readyToPlay {
/// Calling some other external delegates
}
}
}
private func removePeriodicTimeObserver() {
if let token = periodicTimeObserverToken {
self.player?.removeTimeObserver(token)
periodicTimeObserverToken = nil
}
}
private func addPlayerFinishedObserver() {
finishObserverToken = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main) { [weak self] _ in
self?.player?.pause()
}
}
private func removePlayerFinishedObserver() {
if let _ = finishObserverToken, let _player = self.player, let _playerCurrentItem = _player.currentItem {
NotificationCenter.default.removeObserver(_player)
NotificationCenter.default.removeObserver(_playerCurrentItem)
self.finishObserverToken = nil
}
}
public func endSession() {
player?.pause()
removePeriodicTimeObserver()
removePlayerFinishedObserver()
playerItem = nil
player = nil
}
The parent class calls endSession() after stream reaches the end, but self?.player?.pause() from addPlayerFinishedObserver() gets called non stop, anything inside the addObserver closure gets called continuously.
Am I doing something wrong?
I had to remove its token inside the closure:
private func addPlayerFinishedObserver() {
finishObserverToken = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main) { [weak self] _ in
self?.player?.pause()
if let token = self?.finishObserverToken {
NotificationCenter.default.removeObserver(token)
}
}
}

How To UnitTest Combine Cancellables?

How to write unit test for this func loadDemos()?
Here is the code
class BenefitViewModel: ObservableObject {
func loadDemos() {
let testMode = ProcessInfo.processInfo.arguments.contains("testMode")
if testMode {
self.demos = DummyData().decodeDemos()
} else {
cancellables.insert(self.getDemos().sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print(error.localizedDescription)
break
case .finished:
break
}
}, receiveValue: { response in
self.demos = response
print(“Demos: \(response.count)")
}))
}
}
}
At the risk of being a little annoying, I'm going to answer the more general version of your question: how can you unit test a Combine pipeline?
Let's step back and start with some general principles about unit testing:
Don't test Apple's code. You already know what it does. Test your code.
Don't test the network (except in a rare test where you just want to make sure the network is up). Substitute your own class that behaves like the network.
Asynchronous code needs asynchronous testing.
I assume your getDemos does some asynchronous networking. So without loss of generality I can illustrate with a different pipeline. Let's use a simple Combine pipeline that fetches an image URL from the network and stores it in a UIImage instance property (this is intended to be quite parallel to what you are doing with your pipeline response and self.demos). Here's a naive implementation (assume that I have some mechanism for calling fetchImage):
class ViewController: UIViewController {
var image : UIImage?
var storage = Set<AnyCancellable>()
func fetchImage() {
let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
self.getImageNaive(url:url)
}
func getImageNaive(url:URL) {
URLSession.shared.dataTaskPublisher(for: url)
.compactMap { UIImage(data:$0.data) }
.receive(on: DispatchQueue.main)
.sink { completion in
print(completion)
} receiveValue: { [weak self] image in
print(image)
self?.image = image
}
.store(in: &self.storage)
}
}
All very nice, and it works fine, but it isn't testable. The reason is that if we simply call getImageNaive in our test, we will be testing the network, which is unnecessary and wrong.
So let's make this testable. How? Well, in this simple example, we just need to break off the asynchronous publisher from the rest of the pipeline, so that the test can substitute its own publisher that doesn't do any networking. So, for example (again, assume I have some mechanism for calling fetchImage):
class ViewController: UIViewController {
// Output is (data: Data, response: URLResponse)
// Failure is URLError
typealias DTP = AnyPublisher <
URLSession.DataTaskPublisher.Output,
URLSession.DataTaskPublisher.Failure
>
var image : UIImage?
var storage = Set<AnyCancellable>()
func fetchImage() {
let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
self.getImage(url:url)
}
func getImage(url:URL) {
let pub = self.dataTaskPublisher(for: url)
self.createPipelineFromPublisher(pub: pub)
}
func dataTaskPublisher(for url: URL) -> DTP {
URLSession.shared.dataTaskPublisher(for: url).eraseToAnyPublisher()
}
func createPipelineFromPublisher(pub: DTP) {
pub
.compactMap { UIImage(data:$0.data) }
.receive(on: DispatchQueue.main)
.sink { completion in
print(completion)
} receiveValue: { [weak self] image in
print(image)
self?.image = image
}
.store(in: &self.storage)
}
}
You see the difference? It's almost the same, but the pipeline itself is now distinct from the publisher. Our method createPipelineFromPublisher takes as its parameter any publisher of the correct type. This means that we have abstracted out the use of URLSession.shared.dataTaskPublisher, and can substitute our own publisher. In other words, createPipelineFromPublisher is testable!
Okay, let's write the test. My test case contains a method that generates a "mock" publisher that simply publishes some Data wrapped up in the same publisher type as a data task publisher:
func dataTaskPublisherMock(data: Data) -> ViewController.DTP {
let fakeResult = (data, URLResponse())
let j = Just<URLSession.DataTaskPublisher.Output>(fakeResult)
.setFailureType(to: URLSession.DataTaskPublisher.Failure.self)
return j.eraseToAnyPublisher()
}
My test bundle (which is called CombineTestingTests) also has an asset catalog containing a UIImage called mannyTesting. So all I have to do is call ViewController's createPipelineFromPublisher with the data from that UIImage, and check that the ViewController's image property is now that same image, right?
func testImagePipeline() throws {
let vc = ViewController()
let mannyTesting = UIImage(named: "mannyTesting", in: Bundle(for: CombineTestingTests.self), compatibleWith: nil)!
let data = mannyTesting.pngData()!
let pub = dataTaskPublisherMock(data: data)
vc.createPipelineFromPublisher(pub: pub)
let image = try XCTUnwrap(vc.image, "The image is nil")
XCTAssertEqual(data, image.pngData()!, "The image is the wrong image")
}
Wrong! The test fails; vc.image is nil. What went wrong? The answer is that Combine pipelines, even a pipeline that starts with a Just, are asynchronous. Asynchronous pipelines require asynchronous testing. My test needs to wait until vc.image is not nil. One way to do that is with a predicate that watches for vc.image to no longer be nil:
func testImagePipeline() throws {
let vc = ViewController()
let mannyTesting = UIImage(named: "mannyTesting", in: Bundle(for: CombineTestingTests.self), compatibleWith: nil)!
let data = mannyTesting.pngData()!
let pub = dataTaskPublisherMock(data: data)
vc.createPipelineFromPublisher(pub: pub)
let pred = NSPredicate { vc, _ in (vc as? ViewController)?.image != nil }
let expectation = XCTNSPredicateExpectation(predicate: pred, object: vc)
self.wait(for: [expectation], timeout: 10)
let image = try XCTUnwrap(vc.image, "The image is nil")
XCTAssertEqual(data, image.pngData()!, "The image is the wrong image")
}
And the test passes! Do you see the point? The system-under-test here is exactly the right thing, namely, the mechanism that forms a pipeline that receives the output that a data task publisher would emit and sets an instance property of our view controller. We have tested our code and only our code. And we have demonstrated that our pipeline works correctly.
With #Joakim Danielson help on this question How to convert myObject to AnyPublisher<myObject, Never>?. I came up with this answer.
func testDemoData() {
let testDemoData = Just(demoData).eraseToAnyPublisher()
cancellables.insert(testDemoData.sink(receiveCompletion: { [weak self] result in
switch result {
case .failure(let error):
XCTAssert(true)
print(error)
break
case .finished:
break
}
}, receiveValue: { [weak self] response in
XCTAssert((response!.count ) >= 0)
}))
}

iOS13's Combine streams don't flow after operator using schedulers

iOS13's Combine streams of publishers don't appear to be flowing after operator using schedulers.
Here's my code:
import Foundation
import Combine
struct MyPublisher: Publisher {
typealias Output = Int
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber,
Failure == S.Failure,
Output == S.Input {
subscriber.receive(1)
print("called 1")
subscriber.receive(2)
print("called 2")
subscriber.receive(completion: .finished)
print("called finish")
}
}
MyPublisher()
// .receive(on: RunLoop.main) // If this line removed, it will be fine.
// .throttle(for: .milliseconds(1000), scheduler: RunLoop.main, latest: false)) // If this line removed, it will be fine.
// .debounce(for: .milliseconds(1000), scheduler: RunLoop.main)) // If this line removed, it will be fine.
// .delay(for: .milliseconds(1000), scheduler: DispatchQueue.main)) // If this line removed, it will be fine.
.print()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("error:\(error)")
}
}, receiveValue: { num in
print("\(num)")
})
I expected output to be
1
2
finished
but the actual output is nothing.
If I don't use receive or throttle or debounce or delay. The output will be fine.
Is it a bug or something wrong with my code?
I tried with Playground (Xcode 11 beta3).
Subscription:
I'm unsure of why it works in the case of a single thread but you should make sure to call received(subscription:) on the subscriber. If you do not need to handle the subscribers demands you can use Subscribers.empty:
struct MyPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subscriber.receive(subscription: Subscriptions.empty)
_ = subscriber.receive(1)
Swift.print("called 1")
_ = subscriber.receive(2)
Swift.print("called 2")
_ = subscriber.receive(completion: .finished)
Swift.print("called finish")
}
}
AnyCancellable:
You should notice a warning:
Result of call to 'sink(receiveCompletion:receiveValue:)' is unused
That should appear since sink returns an AnyCancellable:
func sink(receiveCompletion: #escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: #escaping ((Self.Output) -> Void)) -> AnyCancellable
Anything that returns an AnyCancellable will get canceled as soon as the AnyCancellable is deallocated.
My speculation is that if you are putting this on another thread, then when the end of the calling method is reached the cancellable will deallocate before the subscription is received. But when received on the current thread it seems to be executing just in time for the subscription and output to show. Most likely the cancellable is being deallocated when the current thread exits.
Use Cancellable
For example :
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private var cancellable: AnyCancellable?
func fetchImages() {
guard let urlString = urlString,
let url = URL(string: urlString) else { return }
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
}
Use the underscore
You can pass the underscore to pass the warning. I've used the example from Naishta's answer.
For example
class ImageLoader: ObservableObject {
#Published var image: UIImage?
func fetchImages() {
guard let urlString = urlString,
let url = URL(string: urlString) else { return }
_ = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
}

Where to put Firebase Performance trace

I am trying to determine what the best location would be for putting a firebase Performance trace. I want to see how long it is taking my app to pull data.
In my VC I have the following
func pullAllUsersCards() {
// 1 Start Here?
FirebaseUtility.shared.getCards { (cards, errMessage) in
if let theCards = cards {
if theCards.count < 1 {
if let addVC = self.storyboard?.instantiateViewController(withIdentifier: StoryboardKeys.addCardViewControllerStoryboardID) as? AddCardViewController {
let addNavigation = UINavigationController(rootViewController: addVC)
if UIDevice.current.userInterfaceIdiom == .pad {
self.splitViewController?.present(addNavigation, animated: true, completion: nil)
} else {
self.present(addNavigation, animated: true, completion: nil)
}
}
} else {
// 2 Start Here?
MBProgressHUD.showAdded(to: self.view, animated: true)
self.cardArray = theCards
self.tableView.reloadData()
MBProgressHUD.hide(for: self.view, animated: true)
}
}
}
}
Originally I wanted to put the trace on my singleton class FirebaseUtility where the getCards method is.
func getCards(completion: #escaping (_ cards: [Card]?, _ errorMessage: String?) -> Void) {
// let testTrace = Performance.startTrace(name: "Test")
guard let userID = user?.uid else {
let error = "Unknown error occured! User is not logged in."
completion(nil, error)
return
}
let userCardRef = ref.child(FirebaseKeys.newCards).child(userID)
userCardRef.observe(.value, with: { (snapshot) in // changed from Single Event
let enumerator = snapshot.children
var cards = [Card]()
while let cardSnapshot = enumerator.nextObject() as? DataSnapshot {
if let cardDict = cardSnapshot.value as? [String : Any] {
let card = Card(id: cardSnapshot.key, cardDict: cardDict)
cards.append(card)
}
}
completion(cards, nil)
})
// testTrace?.stop()
}
however when I try to use it there I get an error saying Firebase Performance does not support Extensions at this time
are you using Firebase Performance in the context of an App Extension (e.g. Watch, keyboard, today, etc.)? That message is triggered by this line in the FirebasePerformance.h file:
NS_EXTENSION_UNAVAILABLE("FirebasePerformance does not support app extensions at this time.")
Firebase Performance currently only supports normal applications on iOS.