IOS Rxswift use Kingfisher to prefetch cell Image - rx-swift

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

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 to get a non-observable value from a function?

I have this function which doesn't work, because immediately returns without setting data
func fetchedData() -> String {
var data: String
networkRequest.suscribe(
onNext: {
data = "successful"
},
onError: {
data = "unsuccesful"
}).addDisposableTo(self.disposeBag)
return data
}
How can I make it work? Sorry I am fairly new to RxSwift
Hope this will helps you.
func fetchedData(isSuccess: #escaping ((Bool) -> Void)) {
networkRequest.suscribe(
onNext: {
isSuccess(true)
},
onError: {
isSuccess(false)
}).addDisposableTo(self.disposeBag)
}
fetchedData { (isSuccess) in
if isSuccess {
print("Successfull")
} else {
print("Unsuccessfull")
}
}
Well, actually what you are trying to achieve can be done with semaphores for example. But your approach is really bad, as I believe you misunderstood the concepts of RxSwift and reactive programming in general.
The first thing you need to understand is that everything in RxSwift is an observable sequence or something that operates on or subscribes to events emitted by an observable sequence.
Arrays, Strings or Dictionaries will be converted to observable sequences in RxSwift. You can create an observable sequence of any Object that conforms to the Sequence Protocol from the Swift Standard Library.
You should not use such function in your code func fetchedData() -> String. In your case you may want to use something like:
func fetchedData() -> Observable<String> {
return networkRequest.map { _ -> String in
return "Successfull"
}.catchErrorJustReturn("Unsuccessfull")
}
Then you have your code aligned with reactive principles. You may bind this sequence to some variables, transform it, share and so on.

Swift 3 dispatch_async or dispatch_sync (dispatch_get_main_queue()) dont work. [duplicate]

I have lots of code in Swift 2.x (or even 1.x) projects that looks like this:
// Move to a background thread to do some long running work
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let image = self.loadOrGenerateAnImage()
// Bounce back to the main thread to update the UI
dispatch_async(dispatch_get_main_queue()) {
self.imageView.image = image
}
}
Or stuff like this to delay execution:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
print("test")
}
Or any of all kinds of other uses of the Grand Central Dispatch API...
Now that I've opened my project in Xcode 8 (beta) for Swift 3, I get all kinds of errors. Some of them offer to fix my code, but not all of the fixes produce working code. What do I do about this?
Since the beginning, Swift has provided some facilities for making ObjC and C more Swifty, adding more with each version. Now, in Swift 3, the new "import as member" feature lets frameworks with certain styles of C API -- where you have a data type that works sort of like a class, and a bunch of global functions to work with it -- act more like Swift-native APIs. The data types import as Swift classes, their related global functions import as methods and properties on those classes, and some related things like sets of constants can become subtypes where appropriate.
In Xcode 8 / Swift 3 beta, Apple has applied this feature (along with a few others) to make the Dispatch framework much more Swifty. (And Core Graphics, too.) If you've been following the Swift open-source efforts, this isn't news, but now is the first time it's part of Xcode.
Your first step on moving any project to Swift 3 should be to open it in Xcode 8 and choose Edit > Convert > To Current Swift Syntax... in the menu. This will apply (with your review and approval) all of the changes at once needed for all the renamed APIs and other changes. (Often, a line of code is affected by more than one of these changes at once, so responding to error fix-its individually might not handle everything right.)
The result is that the common pattern for bouncing work to the background and back now looks like this:
// Move to a background thread to do some long running work
DispatchQueue.global(qos: .userInitiated).async {
let image = self.loadOrGenerateAnImage()
// Bounce back to the main thread to update the UI
DispatchQueue.main.async {
self.imageView.image = image
}
}
Note we're using .userInitiated instead of one of the old DISPATCH_QUEUE_PRIORITY constants. Quality of Service (QoS) specifiers were introduced in OS X 10.10 / iOS 8.0, providing a clearer way for the system to prioritize work and deprecating the old priority specifiers. See Apple's docs on background work and energy efficiency for details.
By the way, if you're keeping your own queues to organize work, the way to get one now looks like this (notice that DispatchQueueAttributes is an OptionSet, so you use collection-style literals to combine options):
class Foo {
let queue = DispatchQueue(label: "com.example.my-serial-queue",
attributes: [.serial, .qosUtility])
func doStuff() {
queue.async {
print("Hello World")
}
}
}
Using dispatch_after to do work later? That's a method on queues, too, and it takes a DispatchTime, which has operators for various numeric types so you can just add whole or fractional seconds:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // in half a second...
print("Are we there yet?")
}
You can find your way around the new Dispatch API by opening its interface in Xcode 8 -- use Open Quickly to find the Dispatch module, or put a symbol (like DispatchQueue) in your Swift project/playground and command-click it, then brouse around the module from there. (You can find the Swift Dispatch API in Apple's spiffy new API Reference website and in-Xcode doc viewer, but it looks like the doc content from the C version hasn't moved into it just yet.)
See the Migration Guide for more tips.
In Xcode 8 beta 4 does not work...
Use:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
print("Are we there yet?")
}
for async two ways:
DispatchQueue.main.async {
print("Async1")
}
DispatchQueue.main.async( execute: {
print("Async2")
})
This one is good example for Swift 4 about async:
DispatchQueue.global(qos: .background).async {
// Background Thread
DispatchQueue.main.async {
// Run UI Updates or call completion block
}
}
in Xcode 8 use:
DispatchQueue.global(qos: .userInitiated).async { }
Swift 5.2, 4 and later
Main and Background Queues
let main = DispatchQueue.main
let background = DispatchQueue.global()
let helper = DispatchQueue(label: "another_thread")
Working with async and sync threads!
background.async { //async tasks here }
background.sync { //sync tasks here }
Async threads will work along with the main thread.
Sync threads will block the main thread while executing.
Swift 4.1 and 5. We use queues in many places in our code. So, I created Threads class with all queues. If you don't want to use Threads class you can copy the desired queue code from class methods.
class Threads {
static let concurrentQueue = DispatchQueue(label: "AppNameConcurrentQueue", attributes: .concurrent)
static let serialQueue = DispatchQueue(label: "AppNameSerialQueue")
// Main Queue
class func performTaskInMainQueue(task: #escaping ()->()) {
DispatchQueue.main.async {
task()
}
}
// Background Queue
class func performTaskInBackground(task:#escaping () throws -> ()) {
DispatchQueue.global(qos: .background).async {
do {
try task()
} catch let error as NSError {
print("error in background thread:\(error.localizedDescription)")
}
}
}
// Concurrent Queue
class func perfromTaskInConcurrentQueue(task:#escaping () throws -> ()) {
concurrentQueue.async {
do {
try task()
} catch let error as NSError {
print("error in Concurrent Queue:\(error.localizedDescription)")
}
}
}
// Serial Queue
class func perfromTaskInSerialQueue(task:#escaping () throws -> ()) {
serialQueue.async {
do {
try task()
} catch let error as NSError {
print("error in Serial Queue:\(error.localizedDescription)")
}
}
}
// Perform task afterDelay
class func performTaskAfterDealy(_ timeInteval: TimeInterval, _ task:#escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: (.now() + timeInteval)) {
task()
}
}
}
Example showing the use of main queue.
override func viewDidLoad() {
super.viewDidLoad()
Threads.performTaskInMainQueue {
//Update UI
}
}

Xcode delegate link to object

I am new to Xcode IDE and to swift and I'm trying to understand better how it works.
I'm aware that it's possible to make a delegation to an UI element, for example, a comboBox. This allows for working with methods that NSComboBoxDelegate supports. That's all fine. I understand how one comboBox can be linked to the method.
My question is: if there are two comboBox elements with delegation to a single .swift file, how do I define which one will "trigger" the method, for example, comboBoxSelectionDidChange? Do I have to have separate .swift files? Does the method work whenever each is changed? Is there a way to link each object to each method, as it's possible to do with outlets and actions?
I believe it's a simple question and probably one that programmers will answer with ease, but for beginners the abstraction and the simpler gapping holes slow comprehension too much.
Your Elements, for example UIPicker, can use the same delegate methods. You need to therefore differentiate between them by a tag. Or by actual reference. You need to give each one of your 'combo boxes' a tag first obviously, before you can use the tag option.
For Example:
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
if(pickerView.tag == 0)
{
//do whatEver you want with the picker that has tag 0
}
else if(pickerView.tag == 1)
{
//do whatEver you want with the picker that has tag 1
}
}
Or by reference:
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
if(pickerView == self.cityPicker)
{
//do whatEver you want with the city picker
}
else if(pickerView = self.countryPicker)
{
//do whatEver you want with the country picker
}
}

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