RXSwift act while debounced - rx-swift

I would like to have an Observable that while debounced (in the time frame where we are still not emitting the value) something else will happen, for example show a spinner.
So in my code example I only reference my view AFTER the value is emitted..
observable.debounce(0.3, scheduler: MainScheduler.instance).do(onNext: { spinner in
spinner.stop() //here I set it to stop, I want to run spinner.start() while we are in the debounce area
}).subscribe().disposedBy(disposeBag)
I thought this question might fit my needs but not sure if it is exactly what I ask for:
RxSwift - Debounce/Throttle "inverse"

As far as I understand the question, the goal is to trigger some action for the timeframe of debouncing/throttling.
It is relatively straightforward to do for throttle (i.e. emitting at most once per timeframe): basically use .window() operator, hang the desired action onto it, and then use the result of .window() for actual throttling.
With debounce (i.e. emitting once after upstream did not emit for a given timeframe), it seems to be complex but probably also doable.

It actually does matter whether you are making a network request or using debounce, because a network request would be done using flatMap and is an independent observable. Here is some sample code that will animate an activity indicator while the network request is in flight:
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
let _activityIndicator = activityIndicator! // to avoid dealing with self.
button.rx.tap
.flatMapLatest { () -> Observable<Int> in
let request = Observable<Int>.timer(5.0, scheduler: MainScheduler.instance)
return Observable.using({ ActivityIndicatorAnimator(_activityIndicator) }, observableFactory: { _ in request })
}
.subscribe()
.disposed(by: bag)
}
let bag = DisposeBag()
}
class ActivityIndicatorAnimator: Disposable {
init(_ spinner: UIActivityIndicatorView) {
self.spinner = spinner
spinner.startAnimating()
}
func dispose() {
spinner.stopAnimating()
}
let spinner: UIActivityIndicatorView
}
The above uses a timer to simulate a network request. The using operator will create a resource when the observable starts and dispose of the resource when the observable completes. The ActivityIndicatorAnimator resource starts the animation when the resource is created and stops it when the resource is disposed.
The RxSwift repository has a more complex example called ActivityIndicator that maintains a count of how many times it was started and stopped and can be used to monitor several network requests.

Related

Use reference to captured variable in concurrently-executing code

Update 2: I suspect the question gets upvoted because of the possible solution that I describe. Highlighted it for clarity.
Update 1: This question gets a lot of views. If you think the question can be enhanced with the situation in which you encountered the error yourself, please briefly describe your situation in the comments so we can make this Q&A more valuable. And if you have a solution to your version of the problem, please add it as an answer.
I want to update the UI after doing async background work using Task.detached and an async function.
However, I get a build error Reference to captured var 'a' in concurrently-executing code error during build.
I tried some things and turning the variable into a let constant before updating the UI is the only thing that works. Why do I need to make a let constant before being able to update the UI? Are there alternatives?
class ViewModel: ObservableObject {
#Published var something: String?
init() {
Task.detached(priority: .userInitiated) {
await self.doVariousStuff()
}
}
private func doVariousStuff() async {
var a = "a"
let b = await doSomeAsyncStuff()
a.append(b)
something = a /* Not working,
Gives
- runtime warning `Publishing changes from
background threads is not allowed; make sure to
publish values from the main thread (via operators
like receive(on:)) on model updates.`
or, if `something` is #MainActor:
- buildtime error `Property 'something' isolated
to global actor 'MainActor' can not be mutated
from this context`
*/
await MainActor.run {
something = a
} /* Not working,
Gives buildtime error "Reference to captured var 'a'
in concurrently-executing code" error during build
*/
DispatchQueue.main.async {
self.something = a
} /* Not working,
Gives buildtime error "Reference to captured var 'a'
in concurrently-executing code" error during build
*/
/*
This however, works!
*/
let c = a
await MainActor.run {
something = c
}
}
private func doSomeAsyncStuff() async -> String {
return "b"
}
}
Make your observable object as main actor, like
#MainActor // << here !!
class ViewModel: ObservableObject {
#Published var something: String?
init() {
Task.detached(priority: .userInitiated) {
await self.doVariousStuff()
}
}
private func doVariousStuff() async {
var a = "a"
let b = await doSomeAsyncStuff()
a.append(b)
something = a // << now this works !!
}
private func doSomeAsyncStuff() async -> String {
return "b"
}
}
Tested with Xcode 13 / iOS 15
In short, something has to be modified from the main thread and only Sendable types can be passed from one actor to another. Let's dig in the details.
something has to be modified from the main thread. This is because #Published properties in an ObservableObject have to be modified from the main thread. The documentation for this is lacking (if anyone finds a link to the official documentation I'll update this answer). But as the subscriber of an ObservableObject is probably a SwiftUI View, it makes sense. Apple could have decided that a View subscribes and receives events on the main thread, but this would hide the fact that it is dangerous to send UI update events from multiple threads.
Only Sendable types can be passed from one actor to another. There are two ways to solve this. First we can make a Sendable. Second we can make sure not to pass a across actor boundaries and have all code run on the same actor (in this case it has to be the Main Actor as it is guaranteed to run on the main thread).
Let's see how to make a sendable and study the case of:
await MainActor.run {
something = a
}
The code in doVariousStuff() function can run from any actor; let's call it Actor A. a belongs to Actor A and it has to be sent to the Main Actor. As a does not conform to Sendable, the compiler does not see any guarantee that a will not be changed while a is read on the Main Actor. This is not allowed in the Swift concurrency model. To give the compiler that guarantee, a has to be Sendable. One way to do that is to make it constant. Which is why this works:
let c = a
await MainActor.run {
something = c
}
Even if it could be improved to:
await MainActor.run { [a] in
something = a
}
Which captures a as a constant. There are other Sendable types, details can be found here https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID649.
The other way to solve this is to make all code run on the same actor. The easiest way to do that is to mark ViewModel with #MainActor as suggested by Asperi. This will guarantee that doVariousStuff() runs from the Main Actor, so it can set something. As a side note, a then belongs to the Main Actor so (even if it is pointless) await MainActor.run { something = a } would work.
Note that actors are not threads. Actor A can run from any thread. It can start on one thread and then continue on another after any await. It could even run partially on the main thread. What is important is that one actor can only ever run from one thread at a time. The only exception to the rule that any actor can run from any thread is for the Main Actor which only runs on the main thread.
You can use #State and .task as follows:
struct ContentView: View {
#State var result = ""
var body: some View {
HStack {
Text(result)
}
.task {
result = await Something.doSomeAsyncStuff()
}
}
}
The task is started when view appears and is cancelled when it disappears. Also if you use .task(id:) it will restart (also cancelling previous task) when the value of id changes.
The async func can go in a few different places, usually somewhere so it can tested independently.
struct Something {
static func doSomeAsyncStuff() async -> String {
return "b"
}
}

How do I implement drag-and-drop from Photos to my Cocoa app with full quality?

In my drag destination view (NSView subclass), I have implemented:
override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
// check for Photos promises
var gotPromised = false
draggingInfo.enumerateDraggingItems(options: [], for: self, classes: [NSFilePromiseReceiver.self], searchOptions: [:], using: {(draggingItem, idx, stop) in
let filePromiseReceiver = draggingItem.item
print("got a file promise receiver: \(filePromiseReceiver)")
gotPromised = true
// Use filePromiseReceiver here for your task.
})
return gotPromised
}
When I run this and drag something over from Photos.app, I get this warning:
How do I fix my drag destination to not have this warning? I would like to get full-quality photos into my app.
Well. Figured it out myself after some investigation. You need to do multiple things (register for correct types, enumerate the items correctly etc), and the warning is not there if you just implement the promise receiving completely and correctly. Apple has provided an example project which implements everything correctly.

IOS Rxswift use Kingfisher to prefetch cell Image

I'm trying to implement Kingfisher prefetch feature inside an Rxswift project. The problem is with these 2 function
collectionView.rx.prefetchItems
collectionView.rx.cancelPrefetchingForItems
The instruction at Kingfisher github is quite short
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.prefetchDataSource = self
}
extension ViewController: UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
let urls = indexPaths.flatMap { URL(string: $0.urlString) }
ImagePrefetcher(urls: urls).start()
}
}
How can we implement with Rxswift? anyway to get the models and then the urls of models from array of indexpath. Thank.
I will walk you thru how I figured out the solution to help you figure out future solutions...
I'm assuming that the struct that holds the URL strings is called Item and that you have an Observable<[Item]> that you are currently using to load up the collection view. I'm also assuming that you only have one section in your collection.
First, we know that something needs to happen when the prefetchItemsAt sends an event so we start with that:
let foo = collectionView.rx.prefetchItems
Now inspect the type of foo to see that it is a ControlEvent<[IndexPath]>. A ControlEvent is a kind of observable. We just need the items part of the IndexPaths, so lets map that:
let foo = collectionView.rx.prefetchItems
.map { $0.map { $0.item } }
(The double map is an unfortunate consequence of Swift not supporting higher kinded types) Now inspect the type of foo. It is an Observable array of ints. Those ints are indexes into our items array. So we need access to the most recently emitted items:
(as above)
.withLatestFrom(items) { indexes, elements in
indexes.map { elements[$0] }
}
So withLatestFrom is like combineLatest except it only fires when the primary observable emits a value, not when the secondary observable does.
Now, inspecting the type of foo will find that it's an Observable array of Items. The exact items who's urls' we want to send to the ImagePrefetcher. So we need to extract the urlStrings into URLs.
(as above)
.map { $0.compactMap { URL(string: $0.urlString) } }
And with that, we have the array of URLs we want ImagePrefetcher to consume. Since it consumes data, it needs to be wrapped it in a subscribe block.
(as above)
.subscribe(onNext: {
ImagePrefetcher(urls: $0).start()
})
At this point, foo is a disposable so it just needs to be collected in our dispose bag... Here is the entire block of code.
collectionView.rx.prefetchItems
.map { $0.map { $0.item } }
.withLatestFrom(items) { indexes, elements in
indexes.map { elements[$0] }
}
.map { $0.compactMap { URL(string: $0.urlString) } }
.subscribe(onNext: {
ImagePrefetcher(urls: $0).start()
})
.disposed(by: bag)
One last bit to make everything clean... Everything between prefetchItems and the subscribe can be moved into the ViewModel if you have one.
The key takeaway here is that you can use the types to guide you, and you need to know what operators are available to manipulate Observables which you can find at http://reactivex.io/documentation/operators.html

AVCam Sample Code of Apple is crashing [duplicate]

I am trying to better understand retain cycles, especially relative to Dispatch Queues. I am working with AVFoundation and managing an AVCaptureSession on a sessionQueue:
private let sessionQueue = DispatchQueue(label: "com.andrewferrarone.sessionQueue")
in a lot of code examples in the apple documentation I see this:
self.sessionQueue.async { [unowned self]
//
}
Is the [unowned self] self here necessary? self (the viewController) references self.sessionQueue and the closure dispatched to self.sessionQueue captures self. Is this a reference cycle? self is not referencing the closure, just the DispatchQueue. If [unowned self] is necessary, then from what I understand, I only want to use unowned self if I am certain that self will not be nil. So lets say I put a task on sessionQueue that takes a long time and the viewController gets popped off and is deallocated before the task finishes? What happens to sessionQueue and the task? If its still around then, when it tries to access self, the app will crash. On the other hand, since unowned self doesn't increment the retain count of self, then it won't prevent the viewController from being deallocated.
So My question is what happens to DispatchQueues when a viewController is deallocated and what happens in this case, if a viewController gets deallocated before a dispatchQueue task is finished? If someone could shed some light on what all is going on here that would be very helpful and appreciated.
Thanks for the help my friends!
Is the [unowned self] self here necessary?
Not only is the use of [unowned self] not necessary, but it's very dangerous in an asynchronously dispatched block. You end up with a dangling pointer to a deallocated object.
If you don't want to keep keep a strong reference to self in an asynchronous call, use [weak self], instead. You should only use unowned if you know the block can never be called after self is deallocated. Obviously, with async call, you don't know this, so [unowned self] should not be used in that context.
Whether you use [weak self] or use strong references is a question of whether you need the asynchronously executed block to keep a strong reference to the object in question or not. For example, if you're updating a view controller's view's controls only, then [weak self] is fine (no point in updating a view that has been dismissed).
The more critical use of weak and unowned references is to avoid strong reference cycles. But that doesn't apply in the example you've provided. You only need to worry about those cycles if the view controller keeps some reference to the blocks itself (e.g. you have some closure property) and those closures reference self, but without a weak/unowned qualifier.
My question is what happens to DispatchQueues when a view controller is deallocated?
Those queues will continue to exist, as will any dispatched blocks, until (a) all dispatched blocks finish; and (b) there are no more strong references to the queue.
So if you asynchronously dispatch blocks with weak references to self (i.e. the view controller), they will continue to run after the view controller is released. This is why it's critical to not use unowned in this context.
For what it's worth, empirical tests can be illuminating. Consider:
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let queue = DispatchQueue(label: "com.domain.app.SecondViewController")
for i in 0 ..< 10 {
queue.async { [weak self] in
print("closure \(i) start")
self?.performSomeTask(i)
print("closure \(i) finish")
}
}
}
private func performSomeTask(_ value: Int) {
print("performSomeTask starting \(value)")
Thread.sleep(forTimeInterval: 5) // you wouldn't generally `sleep`, but merely for diagnostic purposes
print("performSomeTask finishing \(value)")
}
deinit {
print("deinit SecondViewController")
}
}
If you dismiss this view controller while the dispatched blocks are queued up and running, you'll see:
With [weak self], the view controller is retained only until the current dispatched block finishes, the view controller will then be released, and the rest of the blocks will rapidly fire off, but because of [weak self], the performSomeTask won't run after the view controller is dismissed.
If you replace weak with unowned (and obviously remove the ? in self?.performSomeTask(...)), you'll see it crash if you dismiss the view controller before the queued blocks have had a chance to start. This is illustrative of why [unowned self] is so dangerous with asynchronous code.
If you simply remove [weak self] altogether and let it use a implicitly strong reference to self, you'll see it won't deallocate the view controller until all queued blocks finish.

Can I receive a callback whenever an NSPasteboard is written to?

I've read Apple's Pasteboard Programming Guide, but it doesn't answer a particular question I have.
I'm trying to write a Cocoa application (for OS X, not iOS) that will keep track of everything that is written to the general pasteboard (so, whenever any application copies and pastes, but not, say, drags-and-drops, which also makes use of NSPasteboard). I could (almost) accomplish this by basically polling the general pasteboard on a background thread constantly, and checking changeCount. Of course, doing this would make me feel very dirty on the inside.
My question is, is there a way to ask the Pasteboard server to notify me through some sort of callback any time a change is made to the general pasteboard? I couldn't find anything in the NSPasteboard class reference, but I'm hoping it lurks somewhere else.
Another way I could imagine accomplishing this is if there was a way to swap out the general pasteboard implementation with a subclass of NSPasteboard that I could define myself to issue a callback. Maybe something like this is possible?
I would greatly prefer if this were possible with public, App Store-legal APIs, but if using a private API is necessary, I'll take that too.
Thanks!
Unfortunately the only available method is by polling (booo!). There are no notifications and there's nothing to observe for changed pasteboard contents. Check out Apple's ClipboardViewer sample code to see how they deal with inspecting the clipboard. Add a (hopefully not overzealous) timer to keep checking for differences and you've got a basic (if clunky) solution that should be App-Store-Friendly.
File an enhancement request at bugreporter.apple.com to request notifications or some other callback. Unfortunately it wouldn't help you until the next major OS release at the earliest but for now it's polling until we all ask them to give us something better.
There was once a post on a mailing list where the decision against a notification api was described. I can't find it right now though. The bottom line was that probably too many applications would register for that api even though they really wouldn't need to. If you then copy something the whole system goes through the new clipboard content like crazy, creating lots of work for the computer. So i don't think they'll change that behavior anytime soon. The whole NSPasteboard API is internally built around using the changeCount, too. So even your custom subclass of NSPasteboard would still have to keep polling.
If you really want to check if the pasteboard changed, just keep observing the changeCount very half second. Comparing integers is really fast so there's really no performance issue here.
Based on answer provided by Joshua I came up with similar implementation but in swift, here is the link to its gist: PasteboardWatcher.swift
Code snippet from same:
class PasteboardWatcher : NSObject {
// assigning a pasteboard object
private let pasteboard = NSPasteboard.generalPasteboard()
// to keep track of count of objects currently copied
// also helps in determining if a new object is copied
private var changeCount : Int
// used to perform polling to identify if url with desired kind is copied
private var timer: NSTimer?
// the delegate which will be notified when desired link is copied
weak var delegate: PasteboardWatcherDelegate?
// the kinds of files for which if url is copied the delegate is notified
private let fileKinds : [String]
/// initializer which should be used to initialize object of this class
/// - Parameter fileKinds: an array containing the desired file kinds
init(fileKinds: [String]) {
// assigning current pasteboard changeCount so that it can be compared later to identify changes
changeCount = pasteboard.changeCount
// assigning passed desired file kinds to respective instance variable
self.fileKinds = fileKinds
super.init()
}
/// starts polling to identify if url with desired kind is copied
/// - Note: uses an NSTimer for polling
func startPolling () {
// setup and start of timer
timer = NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: Selector("checkForChangesInPasteboard"), userInfo: nil, repeats: true)
}
/// method invoked continuously by timer
/// - Note: To keep this method as private I referred this answer at stackoverflow - [Swift - NSTimer does not invoke a private func as selector](http://stackoverflow.com/a/30947182/217586)
#objc private func checkForChangesInPasteboard() {
// check if there is any new item copied
// also check if kind of copied item is string
if let copiedString = pasteboard.stringForType(NSPasteboardTypeString) where pasteboard.changeCount != changeCount {
// obtain url from copied link if its path extension is one of the desired extensions
if let fileUrl = NSURL(string: copiedString) where self.fileKinds.contains(fileUrl.pathExtension!){
// invoke appropriate method on delegate
self.delegate?.newlyCopiedUrlObtained(copiedUrl: fileUrl)
}
// assign new change count to instance variable for later comparison
changeCount = pasteboard.changeCount
}
}
}
Note: in the shared code I am trying to identify if user has copied a
file url or not, the provided code can easily be modified for other general
purposes.
For those who need simplified version of code snippet that gets the job done in Swift 5.7,
it just works (base on #Devarshi code):
func watch(using closure: #escaping (_ copiedString: String) -> Void) {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
guard let copiedString = pasteboard.string(forType: .string),
pasteboard.changeCount != changeCount else { return }
defer {
changeCount = pasteboard.changeCount
}
closure(copiedString)
}
}
how to use is as below:
watch {
print("detected : \($0)")
}
then if you attempt copy any text in your pasteboard, it will watch and print out to the console like below..
detected : your copied message in pasteboard
detected : your copied message in pasteboard
in case, full code sample for how to use it for example in SwiftUI:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
watch {
print("detect : \($0)")
}
}
}
}
func watch(using closure: #escaping (_ copiedString: String) -> Void) {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
guard let copiedString = pasteboard.string(forType: .string),
pasteboard.changeCount != changeCount else { return }
defer {
changeCount = pasteboard.changeCount
}
closure(copiedString)
}
}
}
It's not necessary to poll. Pasteboard would generally only be changed by the current view is inactive or does not have focus. Pasteboard has a counter that is incremented when contents change. When window regains focus (windowDidBecomeKey), check if changeCount has changed then process accordingly.
This does not capture every change, but lets your application respond if the Pasteboard is different when it becomes active.
In Swift...
var pasteboardChangeCount = NSPasteboard.general().changeCount
func windowDidBecomeKey(_ notification: Notification)
{ Swift.print("windowDidBecomeKey")
if pasteboardChangeCount != NSPasteboard.general().changeCount
{ viewController.checkPasteboard()
pasteboardChangeCount = NSPasteboard.general().changeCount
}
}
I have a solution for more strict case: detecting when your content in NSPasteboard was replaced by something else.
If you create a class that conforms to NSPasteboardWriting and pass it to -writeObjects: along with the actual content, NSPasteboard will retain this object until its content is replaced. If there are no other strong references to this object, it get deallocated.
Deallocation of this object is the moment when new NSPasteboard got new content.

Resources