I create 2 operations, let's say CKModifySubscriptionsOperation. One is for private, another for shared database. I could queue them by adding to the OperationQueue, each next would start after previous completion block.
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
// ...
operationQueue.addOperation(operation)
// Queued great but all subscriptions are created in private database
But I need to do some action (fetching, modifying etc) from different databases, but still need to queue them. Here is how I add operation to the database. How to put them to the single queue but still let them go to needed database each?
container.privateCloudDatabase.add(operation)
container.sharedCloudDatabase.add(operation)
// Put subscriptions to correct databases but no queue
I've solved the goal by creating a controlled custom operation.
Now we can queue cloud database specific operations just like that
let privateSubscriptionOperation = SubscriptionOperation(type: .private)
let sharedSubscriptionOperation = SubscriptionOperation(type: .shared)
operationQueue.addOperation(privateSubscriptionOperation)
operationQueue.addOperation(sharedSubscriptionOperation)
First of all, parent class
class CKDatabaseControlledOperation: Operation {
let databaseType: DatabaseType
let database: CKDatabase
private var _finished = false
private var _executing = false
init(type: DatabaseType) {
databaseType = type
switch type {
case .private:
database = CKContainer.default().privateCloudDatabase
case .shared:
database = CKContainer.default().sharedCloudDatabase
}
}
override var isExecuting: Bool {
get {
return !_executing
}
set {
willChangeValue(forKey: "isExecuting") // This must match the overriden variable
_executing = newValue
didChangeValue(forKey: "isExecuting") // This must match the overriden variable
}
}
override var isFinished: Bool {
get {
return _finished
}
set {
willChangeValue(forKey: "isFinished") // This must match the overriden variable
_finished = newValue
didChangeValue(forKey: "isFinished") // This must match the overriden variable
}
}
func stopOperation() {
isFinished = true
isExecuting = false
}
func startOperation() {
isFinished = false
isExecuting = true
}
enum DatabaseType: String {
case `private` = "private-changes"
case shared = "shared-changes"
}
}
And then we can create any database operation (subscription in this example but will work with any)
class SubscriptionOperation: CKDatabaseControlledOperation {
override func main() {
startOperation() //Operation starts
let subscription = CKDatabaseSubscription(subscriptionID: databaseType.rawValue)
//...set any needed stuff like NotificationInfo
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: [])
operation.modifySubscriptionsCompletionBlock = { [unowned self] subscriptions, subscriptionIDs, error in
//Handle errors
self.stopOperation() //Operation ends
}
database.add(operation)
}
}
Related
Does anyone have any references on unit testing key value observing logic in swift? I'd like to mock the AVPlayerItem's status changes in my unit tests to confirm that the expected logic is triggered. For simplicity this is what I have so far:
A function that observes the provided playerItem's status
public func observePlayerItemStatus(playerItemToObserve: AVPlayerItem) -> NSKeyValueObservation {
let observerToken = playerItemToObserve.observe(\.status, options: [.new, .old], changeHandler: { (playerItemToObserve, _) in
switch playerItemToObserve.status {
case .failed:
// failed logic
case .readyToPlay:
// ready to play logic
default:
// default logic
}
})
return observerToken
}
I've created a mock class, MockAVPlayerItem
class MockAVPlayerItem: AVPlayerItem {
var mockStatus: AVPlayerItem.Status = .unknown
public override var status: AVPlayerItem.Status {
get { return mockStatus }
set { self.mockStatus = newValue }
}
}
My unit test attempting to trigger the status code change does the following:
func test_readyToPlayIsTriggered() {
let url = URL(string: "https://www.rmp-streaming.com/media/big-buck-bunny-360p.mp4")!
let mockPlayerItem = MockPlayerItem(url: url)
let statusToken = observePlayerItemStatus(playerItemToObserve: mockPlayerItem)
mockPlayerItem.status = .readyToPlay
... some assertion that ready to play was called ....
}
The logic itself works when I run the application however my unit tests are unable to trigger my observer. I've followed a similar approach to mock AVPlayer.timeControlStatus and it's worked great but for some reason AVPlayerItem.status isn't behaving the same.
Using a willChangeValue and didChangeValue in my mock solved my problem. Observers are now triggered as expected.
class MockAVPlayerItem: AVPlayerItem {
var mockStatus: AVPlayerItem.Status = .unknown
public override var status: AVPlayerItem.Status {
get { return mockStatus }
set {
willChangeValue(for: \.status)
self.mockStatus = newValue
didChangeValue(for: \.status)
}
}
}
I'm using NSColorPanel to change the color of a view.
The color of this view is also saved in a database (Firestore).
import AppKit
class ColorPanel {
static var shared = ColorPanel()
private var stage: DB.Stage.Document? = nil
private let cp = NSColorPanel.shared
init() {
cp.setTarget(self)
cp.setAction(#selector(colorDidChange(sender:)))
cp.isContinuous = false
}
func show(stage: DB.Stage.Document) {
self.stage = stage
cp.makeKeyAndOrderFront(nil)
}
#objc func colorDidChange(sender: NSColorPanel) {
guard let stage = stage else { return }
stage.data?.color.red = Double(sender.color.redComponent)
stage.data?.color.green = Double(sender.color.greenComponent)
stage.data?.color.blue = Double(sender.color.blueComponent)
stage.update()
}
}
The problem is that I would like to set isContinuos true in order to see my view changing color real-time, but is sending too much updates to the server, so I have been forced to set it false.
There is a way to solve this? I just need to do an update when I finish the drag but I don't know how.
p.s. To call the ColorPanel in my SwiftUI view I do:
ColorPanel.shared.show(stage: stage)
Please try an approach I would use. Disclaimer: not tested due to absent Firestore setup
import Combine
class ColorPanel {
static var shared = ColorPanel()
private var stage: DB.Stage.Document? = nil
private let cp = NSColorPanel.shared
private var subscriber: AnyCancellable?
private let publisher =
PassthroughSubject<NSColor, Never>()
.throttle(for: 10, scheduler: RunLoop.main, latest: true)
init() {
cp.setTarget(self)
cp.setAction(#selector(colorDidChange(sender:)))
cp.isContinuous = true
}
func show(stage: DB.Stage.Document) {
self.stage = stage
self.subscriber = nil
if stage != nil {
self.subscriber = self.publisher
.sink { _ in
self.stage.update() // << be called once per 10 seconds
}
}
cp.makeKeyAndOrderFront(nil)
}
#objc func colorDidChange(sender: NSColorPanel) {
guard let stage = stage else { return }
stage.data?.color.red = Double(sender.color.redComponent)
stage.data?.color.green = Double(sender.color.greenComponent)
stage.data?.color.blue = Double(sender.color.blueComponent)
self.publisher.upstream.send(sender.color)
}
}
I have a CoreLocation manager that should handle all CLLocationManager by offering observable properties through RxSwift (and its Extensions and DelegateProxies). LocationRepository looks like this:
class LocationRepository {
static let sharedInstance = LocationRepository()
var locationManager: CLLocationManager = CLLocationManager()
private (set) var supportsRequiredLocationServices: Driver<Bool>
private (set) var location: Driver<CLLocationCoordinate2D>
private (set) var authorized: Driver<Bool>
private init() {
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
supportsRequiredLocationServices = Observable.deferred {
let support = CLLocationManager.locationServicesEnabled() && CLLocationManager.significantLocationChangeMonitoringAvailable() && CLLocationManager.isMonitoringAvailable(for:CLCircularRegion.self)
return Observable.just(support)
}
.asDriver(onErrorJustReturn: false)
authorized = Observable.deferred { [weak locationManager] in
let status = CLLocationManager.authorizationStatus()
guard let locationManager = locationManager else {
return Observable.just(status)
}
return locationManager.rx.didChangeAuthorizationStatus.startWith(status)
}
.asDriver(onErrorJustReturn: CLAuthorizationStatus.notDetermined)
.map {
switch $0 {
case .authorizedAlways:
return true
default:
return false
}
}
location = locationManager.rx.didUpdateLocations.asDriver(onErrorJustReturn: []).flatMap {
return $0.last.map(Driver.just) ?? Driver.empty()
}
.map { $0.coordinate }
}
func requestLocationPermission() {
locationManager.requestAlwaysAuthorization()
}
}
My presenter then listens to changes on the repository properties. LocatorPresenter looks like this:
class LocatorPresenter: LocatorPresenterProtocol {
weak var view: LocatorViewProtocol?
var repository: LocationRepository?
let disposeBag = DisposeBag()
func handleLocationAccessPermission() {
guard repository != nil, view != nil else {
return
}
repository?.authorized.drive(onNext: {[weak self] (authorized) in
if !authorized {
print("not authorized")
if let sourceView = self?.view! as? UIViewController, let authorizationView = R.storyboard.locator.locationAccessRequestView() {
sourceView.navigationController?.present(authorizationView, animated: true)
}
} else {
print("authorized")
}
}).addDisposableTo(disposeBag)
}
}
It does work, but I'm getting the Driver calling twice for the first time I try to get the authorization status, so the access request view gets presented twice. What am I missing here?
Regards!
From startWith documentation:
StartWith
emit a specified sequence of items before beginning to emit the items from the source Observable
I have not tried it, but probably if you remove startWith(status) you won't receive the status twice.
It seems you are receiving the next sequence from the observable:
---------------------------------unauthorized----authorized----->
So with the line:
startWith(status) // status is unauthorized
you finally get this one:
-------unauthorized---------unauthorized----authorized----->
I have a AsyncOperation class defined as such
import Foundation
class ASyncOperation: NSOperation {
enum State: String {
case Ready, Executing, Finished
private var keyPath: String {
return "is" + rawValue
}
}
var state = State.Ready {
willSet {
willChangeValueForKey(newValue.keyPath)
willChangeValueForKey(state.keyPath)
}
didSet {
didChangeValueForKey(oldValue.keyPath)
didChangeValueForKey(state.keyPath)
}
}
override var ready: Bool {
return super.ready && state == .Ready
}
override var executing: Bool {
return super.ready && state == .Executing
}
override var finished: Bool {
return super.ready && state == .Finished
}
override var asynchronous: Bool {
return true
}
override func start() {
if cancelled {
state = .Finished
return
}
main()
state = .Executing
}
override func cancel() {
state = .Finished
}
}
and a subclass of it ImageLoadOperation.
import Foundation
import UIKit
import Firebase
class ImageLoadOperation: ASyncOperation {
var imagePath: String?
var image: UIImage?
override func main(){
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://salisbury-zoo- 91751.appspot.com")
if let path = imagePath {
let imageReference = storageRef.child(path)
imageReference.dataWithMaxSize(3 * 1024 * 1024) { (data, error) -> Void in
if (error != nil) {
self.image = nil
} else {
self.image = UIImage(data: data!)
self.state = .Finished
}
}
}
}
}
So I go to call the Operation in a Queue
let queue = NSOperationQueue()
let imageLoad = ImageLoadOperation()
queue.addOperation(imageLoad)
let img:UIImage? = imageLoad.image
But it always returns nil. When I put a print statement in the callback of ImageLoadOperation the image is there and state is set to finished. When I add
queue.waitUntilAllOperationsAreFinished()
Inbetween queue.addOperation and let img:UIImage? = imageLoad.load then the entire application stalls as the main thread is blocked. Any other ideas on how I could get the image to be there outside the scope of the callback? I have also tried doing it without a NSOperationQueue and just as an NSOperation with no luck.
The queue.addOperation function adds the operation, and it starts executing in a background thread. It therefore returns well before the background thread is finished, which is why the image is nil.
And as the documentation states, waitUntilAllOperationsAreFinished will block the thread until the operations are finished. This is very undesirable on the main thread.
imageReference.dataWithMaxSize is an asynchronous operation that has a completion handler (where you are currently setting self.image). You need something in there to trigger code to run that will allow you to use imageLoad.image. How you do this will depend on the architecture of your app.
If your image is to be displayed in a UITableViewCell, for example, you will need to store the image in an array of images, possibly where the index matches the table row, and then reload at least that row of the tableView. This is because by the time the image has been received, the cell may no longer exist for that row. Obviously you would not want this code sitting inside your ImageLoadOperation class. Instead it should be passed into main() as a completion handler.
Rob provided a great Objective-C solution for subclassing NSOperation to achieve a serial queuing mechanism for SKAction objects. I implemented this successfully in my own Swift project.
import SpriteKit
class ActionOperation : NSOperation
{
let _node: SKNode // The sprite node on which an action is to be performed
let _action: SKAction // The action to perform on the sprite node
var _finished = false // Our read-write mirror of the super's read-only finished property
var _executing = false // Our read-write mirror of the super's read-only executing property
/// Override read-only superclass property as read-write.
override var executing: Bool {
get { return _executing }
set {
willChangeValueForKey("isExecuting")
_executing = newValue
didChangeValueForKey("isExecuting")
}
}
/// Override read-only superclass property as read-write.
override var finished: Bool {
get { return _finished }
set {
willChangeValueForKey("isFinished")
_finished = newValue
didChangeValueForKey("isFinished")
}
}
/// Save off node and associated action for when it's time to run the action via start().
init(node: SKNode, action: SKAction) {
// This is equiv to ObjC:
// - (instancetype)initWithNode(SKNode *)node (SKAction *)action
// See "Exposing Swift Interfaces in Objective-C" at https://developer.apple.com/library/mac/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html#//apple_ref/doc/uid/TP40014216-CH4-XID_35
_node = node
_action = action
super.init()
}
/// Add the node action to the main operation queue.
override func start()
{
if cancelled {
finished = true
return
}
executing = true
NSOperationQueue.mainQueue().addOperationWithBlock {
self._node.runAction(self._action) {
self.executing = false
self.finished = true
}
}
}
}
To use the ActionOperation, instantiate an NSOperationQueue class member in your client class:
var operationQueue = NSOperationQueue()
Add this important line in your init method:
operationQueue.maxConcurrentOperationCount = 1; // disallow follow actions from overlapping one another
And then when you are ready to add SKActions to it such that they run serially:
operationQueue.addOperation(ActionOperation(node: mySKNode, action: mySKAction))
Should you need to terminate the actions at any point:
operationQueue.cancelAllOperations() // this renders the queue unusable; you will need to recreate it if needing to queue anymore actions
Hope that helps!
According to the document:
In your custom implementation, you must generate KVO notifications for the isExecuting key path whenever the execution state of your operation object changes.
In your custom implementation, you must generate KVO notifications for the isFinished key path whenever the finished state of your operation object changes.
So I think you have to:
override var executing:Bool {
get { return _executing }
set {
willChangeValueForKey("isExecuting")
_executing = newValue
didChangeValueForKey("isExecuting")
}
}
override var finished:Bool {
get { return _finished }
set {
willChangeValueForKey("isFinished")
_finished = newValue
didChangeValueForKey("isFinished")
}
}
I want to group animations for several nodes. I first tried the solution above by grouping all actions in one, using runAction(_:onChildWithName:) to specify which actions have to been done by node.
Unfortunately there were synchronisation problems because in the case of runAction(_:onChildWithName:) the duration for SKAction is instantaneous. So I have to found another way to group animations for several nodes in one operation.
I then modified the code above by adding an array of tuples (SKNode,SKActions).
The modified code presented here add the feature to init the operation for several nodes, each one of them having it's own actions.
For each node action is run inside it's own block added to the operation using addExecutionBlock.
When an action complete, a completion block is executed calling checkCompletion() in order to join them all. When all actions have completed then the operation is marked as finished.
class ActionOperation : NSOperation
{
let _theActions:[(SKNode,SKAction)]
// The list of tuples :
// - SKNode The sprite node on which an action is to be performed
// - SKAction The action to perform on the sprite node
var _finished = false // Our read-write mirror of the super's read-only finished property
var _executing = false // Our read-write mirror of the super's read-only executing property
var _numberOfOperationsFinished = 0 // The number of finished operations
override var executing:Bool {
get { return _executing }
set {
willChangeValueForKey("isExecuting")
_executing = newValue
didChangeValueForKey("isExecuting")
}
}
override var finished:Bool {
get { return _finished }
set {
willChangeValueForKey("isFinished")
_finished = newValue
didChangeValueForKey("isFinished")
}
}
// Initialisation with one action for one node
//
// For backwards compatibility
//
init(node:SKNode, action:SKAction) {
_theActions = [(node,action)]
super.init()
}
init (theActions:[(SKNode,SKAction)]) {
_theActions = theActions
super.init()
}
func checkCompletion() {
_numberOfOperationsFinished++
if _numberOfOperationsFinished == _theActions.count {
self.executing = false
self.finished = true
}
}
override func start()
{
if cancelled {
finished = true
return
}
executing = true
_numberOfOperationsFinished = 0
var operation = NSBlockOperation()
for (node,action) in _theActions {
operation.addExecutionBlock({
node.runAction(action,completion:{ self.checkCompletion() })
})
}
NSOperationQueue.mainQueue().addOperation(operation)
}
}
There is a limitation case when SKActions transmitted during initialization is a runAction(_:onChildWithName:).
In this case the duration for this SKAction is instantaneous.
According to Apple documentation:
This action has an instantaneous duration, although the action executed on the child may have a duration of its own.