RxSwift Using Variables Correctly - swift

I am trying to convert a project to use RxSwift and MVVM. I have a service that syncs a list of data from Parse on every app launch and I basically want to make sure I am taking the correct approach.
What I have done is made a Variable subject and then allow my models to listen to this.
ParseService:
let rx_parseMushrooms = Variable<[ParseMushroom]>([])
MushroomLibraryModel:
_ = parseService.rx_parseMushrooms
.asObservable()
.map { (parseMushrooms:[ParseMushroom]) -> [Mushroom] in
let mushrooms = parseMushrooms.map { (parseMushroom:ParseMushroom) -> Mushroom in
let mushroom = Mapper<Mushroom>().map(parseMushroom.dictionaryWithValuesForKeys(parseMushroom.allKeys()))
return mushroom!
}
return mushrooms
}
.subscribeNext({ (mushrooms:[Mushroom]) -> Void in
self.mushrooms = mushrooms
print(mushrooms)
})
I do the same for expressing the sync state.
ParseService:
struct SyncState {
enum State {
case Unsynced, ConnectingToServer, SyncingInfo, FetchingImageList, SyncingImages, SyncComplete, SyncCompleteWithError
}
var infoToSync = 0
var imagesToSync = 0
var imagesSynced = 0
var state = State.Unsynced
}
let rx_syncState = Variable(SyncState())
I then update the variable a la
self.rx_syncState.value = self.syncState
SyncViewModel:
_ = parseService.rx_syncState
.asObservable()
.subscribeNext { [weak self] (syncState:ParseService.SyncState) -> Void in
switch syncState.state {
//show stuff based on state struct
}
}
Anyways, I would greatly appreciate if someone can tell me if this is a good way of going about it or if I am misusing RxSwift (and guide me on how I should be doing this).
Cheers!

Hmm... Here is an article about using Variable (note that Variable is a wrapper around BehaviorSubject.)
http://davesexton.com/blog/post/To-Use-Subject-Or-Not-To-Use-Subject.aspx
In your case, you already have a cold observable (the network call,) so you don't need a Subject/Variable. All you need to do is publish the observable you already have and use replay(1) to cache the value. I would expect a class named something like ParseServer that contains a computed property named something like mushrooms.
To help get the mushrooms out of parse, you could use this (this will create the cold observable you need):
extension PFQuery {
var rx_findObjects: Observable<[PFObject]> {
return Observable.create { observer in
self.findObjectsInBackgroundWithBlock({ results, error in
if let results = results {
observer.on(.Next(results))
observer.on(.Completed)
}
else {
observer.on(.Error(error ?? RxError.Unknown))
}
})
return AnonymousDisposable({ self.cancel() })
}
}
}
And then you would have something like:
class ParseServer {
var mushrooms: Observable<[Mushroom]> {
return PFQuery(className: "Mushroom").rx_findObjects
.map { $0.map { Mushroom(pfObject: $0) } }
.publish()
.replay(1)
}
}
I think the above is correct. I didn't run it through a compiler though, much less test it. It might need editing.
The idea though is that the first time you call myParseServer.mushrooms the system will call Parse to get the mushrooms out and cache them. From then on, it will just return the previous cashed mushrooms.

Related

How to check for staleness of data in Combine with Timers

I am converting some code into Combine in order to get familiar with it. I am doing fine with the easy stuff, but here it gets a little trickier. I am trying to report to the user when incoming GPS data is accurate and also whether it's stale.
So I have
let locationPublisher = PassthroughSubject<CLLocation,Never>()
private var cancellableSet: Set<AnyCancellable> = []
var status:GPSStatus = .red //enum
and in init I have
locationPublisher
.map(gpsStatus(from:)) //maps CLLocation to GPSStatus enum
.assign(to: \.gpsStatus, on: self)
.store(in: &cancellableSet)
locationPublisher.sink() { [weak self] location in
self?.statusTimer?.invalidate()
self?.setStatusTimer()
}
.store(in: &cancellableSet)
setStatusTimer()
Here is the setStatusTimer function
func setStatusTimer () {
statusTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) {#MainActor _ in
self.updateGPSStatus(.red)
}
}
Is there a more "Combine" way of doing this? I know there are Timer.TimerPublishers, but I'm not sure how to incorporate them?
My tendency is to think there is some kind of combineLatest with one input being the gps status publisher and the other one being some kind go publisher that fires if the upstream pub hasn't fired for x seconds.
Thanks!
This is a bit tricky. You don't need a TimerPublisher but you can use the timeout operator. The tricky part is that timeout will create a publisher that stops publishing when it times out. The question is "how do you start again". To do that, you can use the catch operator.
The solution looks like this:
import UIKit
import Combine
enum GPSStatus {
// Karma Chameleon...
case red
case gold
case green
}
func gpsStatus(from location: String) -> GPSStatus {
switch location {
case "ok":
return .green
default:
return .gold
}
}
class UnnamedLocationThingy {
let locationPublisher = PassthroughSubject<String,Never>()
var gpsStatus:GPSStatus = .red {
didSet { print("set the new value to \(String(describing: gpsStatus))") }
}
private enum LocationError: Error {
case timeout
}
private var statusProvider: AnyCancellable?
init() {
statusProvider = makeStatusProvider()
.assign(to: \.gpsStatus, on: self)
}
func makeStatusProvider() -> AnyPublisher<GPSStatus, Never> {
return locationPublisher
.map(gpsStatus(from:)) //maps CLLocation to GPSStatus enum
.setFailureType(to: LocationError.self)
.timeout(.seconds(2), scheduler: DispatchQueue.main) {
return LocationError.timeout
}
.catch { _ in
self.gpsStatus = .red;
return self.makeStatusProvider()
}.eraseToAnyPublisher()
}
}
let thingy = UnnamedLocationThingy();
Task {
for delay in [1, 1, 1, 3, 1, 1, 3, 1] {
try await Task.sleep(for: .seconds(delay))
thingy.locationPublisher.send("ok")
}
}
The heart of it is the makeStatusProvider function. This function creates a publisher that will convert published locations to GPSStatus values as long as they come in in a time limit. But if one doesn't come fast enough, it times out. I set up the timeout operator with a customError: handler so that it doesn't just terminate the publisher, but sends an error. I can catch that error in the catch operator and substitute a new publisher to replace the old one. The new publisher I substitute is a brand new publisher created by makeStatusProvider which, as we've just seen is a publisher that converts published locations into GPSStatus values until it hits a timeout...
It's a form of recursion.
I've decorated your code with enough stuff to make a Playground and added a bit of code at the end to exercise the functionality.

Unable to infer complex closure return type; add explicit type to disambiguate in RxSwift

I need to make multiple calls.
1. Delete Document Upload
2. Image 1 & server returns URL
3. Upload Image 2 & server returns URL
4. Create Document API contains both URLs & extra
parameters.
The code which I tried to write is in RxSwift,& MVVM.
let resultOfDocumentUpdateWithDelete =
donepressed
.filter{ $0 }
.withLatestFrom(self.existingDocumentIDChangedProperty)
.flatMapLatest {id in
let deleted_document = apiClient.deleteDocument(id).asObservable().materialize()
let upload_frontImage = deleted_document
.withLatestFrom(self.frontImageNameChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_backImage = upload_frontImage
.withLatestFrom(self.backImageChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_document = upload_backImage
.withLatestFrom(self.parametersChangedProperty)
.flatMapLatest {parameters in
apiClient.uploadDocument(parameters: parameters)
}
return upload_document.materialize()
}
.share(replay: 1)
Make sure, two responses of server are input in last API, so all of these will be called in a sequence.
how to do in RxSwift.
This was an interesting one! The take-away here is that when you are in doubt, go ahead and make your own operator. If it turns out that you later figure out how to do the job using the built-in operators, then you can replace yours. The only thing with making your own is that they require a lot more testing.
Note, to use the below, you will have to combineLatest of your observables and then flatMap and pass their values into this function.
// all possible results from this job.
enum ProcessResult {
case success
case deleteFailure(Error)
case imageFailue(Error)
case backImageFailure(Error)
case documentFailure(Error)
}
func uploadContent(apiClient: APIClient, documentID: Int, frontImage: UIImage, backImage: UIImage, parameters: Parameters) -> Single<ProcessResult> {
// instead of trying to deal with all the materializes, I decided to turn it into a single process.
return Single.create { observer in
// each api call happens in turn. Note that there are no roll-back semantics included! You are dealing with a very poorly written server.
let deleted = apiClient.deleteDocument(id: documentID)
.asObservable()
.share()
let imagesUploaded = deleted
.flatMap { _ in Observable.zip(apiClient.uploadImage(image: frontImage).asObservable(), apiClient.uploadImage(image: backImage).asObservable()) }
.share()
let documentUploaded = imagesUploaded
.flatMap { arg -> Single<Void> in
let (frontURL, backURL) = arg
var updatedParams = parameters
// add frontURL and backURL to parameters
return apiClient.uploadDocument(parameters: updatedParams)
}
.share()
let disposable = deleted
.subscribe(onError: { observer(.success(ProcessResult.deleteFailure($0))) })
let disposable1 = imagesUploaded
.subscribe(onError: { observer(.success(ProcessResult.imageFailue($0))) })
let disposable2 = documentUploaded
.subscribe(
onNext: { observer(.success(ProcessResult.success)) },
onError: { observer(.success(ProcessResult.documentFailure($0))) }
)
return Disposables.create([disposable, disposable1, disposable2])
}
}

Closures for waiting data from CloudKit

I have a CloudKit database with some data. By pressing a button my app should check for existence of some data in the Database. The problem is that all processes end before my app get the results of its search. I found this useful Answer, where it is said to use Closures.
I tried to follow the same structure but Swift asks me for parameters and I get lost very quick here.
Does someone can please help me? Thanks for any help
func reloadTable() {
self.timePickerView.reloadAllComponents()
}
func getDataFromCloud(completionHandler: #escaping (_ records: [CKRecord]) -> Void) {
print("I begin asking process")
var listOfDates: [CKRecord] = []
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Riservazioni", predicate: predicate)
let queryOperation = CKQueryOperation(query: query)
queryOperation.resultsLimit = 20
queryOperation.recordFetchedBlock = { record in
listOfDates.append(record)
}
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil {
print("error")
print(error!.localizedDescription)
} else {
print("NO error")
self.Array = listOfDates
completionHandler(listOfDates)
}
}
}
var Array = [CKRecord]()
func generateHourArray() {
print("generate array")
for hour in disponibleHours {
let instance = CKRecord(recordType: orderNumber+hour)
if Array.contains(instance) {
disponibleHours.remove(at: disponibleHours.index(of: hour)!)
}
}
}
func loadData() {
timePickerView.reloadAllComponents()
timePickerView.isHidden = false
}
#IBAction func checkDisponibility(_ sender: Any) {
if self.timePickerView.isHidden == true {
getDataFromCloud{ (records) in
print("gotData")
self.generateHourArray()
self.loadData()
}
print(Array)
}
}
Im struggling to understand your code and where the CloudKit elements fit in to it, so Im going to try and give a generic answer which will hopefully still help you.
Lets start with the function we are going to call to get our CloudKit data, lets say we are fetching a list of people.
func getPeople() {
}
This is simple enough so far, so now lets add the CloudKit code.
func getPeople() {
var listOfPeople: [CKRecord] = [] // A place to store the items as we get them
let query = CKQuery(recordType: "Person", predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.resultsLimit = 20
// As we get each record, lets store them in the array
queryOperation.recordFetchedBlock = { record in
listOfPeople.append(record)
}
// Have another closure for when the download is complete
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil {
print(error!.localizedDescription)
} else {
// We are done, we will come back to this
}
}
}
Now we have our list of people, but we want to return this once CloudKit is done. As you rightly said, we want to use a closure for this. Lets add one to the function definition.
func getPeople(completionHandler: #escaping (_ records: [CKRecord]) -> Void) {
...
}
This above adds a completion hander closure. The parameters that we are going to pass to the caller are the records, so we add that into the definition. We dont expect anyone to respond to our completion handler, so we expect a return value of Void. You may want a boolean value here as a success message, but this is entirely project dependent.
Now lets tie the whole thing together. On the line I said we would come back to, you can now replace the comment with:
completionHandler(listOfPeople)
This will then send the list of people to the caller as soon as CloudKit is finished. Ive shown an example below of someone calling this function.
getPeople { (records) in
// This code wont run until cloudkit is finished fetching the data!
}
Something to bare in mind, is which thread the CloudKit API runs on. If it runs on a background thread, then the callback will also be on the background thread - so make sure you don't do any UI changes in the completion handler (or move it to the main thread).
There are lots of improvements you could make to this code, and adapt it to your own project, but it should give you a start. Right off the bat, Id image you will want to change the completion handler parameters to a Bool to show whether the data is present or not.
Let me know if you notice any mistakes, or need a little more help.

RxSwift : how to Unit Test Searching in Searchbar and displaying results in tableview

So ive been using Rxswift for a while and its been working well. Ive managed to get all my code under test but Im struggling to figure out how to test searching with searchbar.rx.bindTo .
There are many tutorials of how to use RxSwift for searching and returning results on a tableview but in none of those tutorials do they show you how to unit test it.
https://www.thedroidsonroids.com/blog/ios/rxswift-by-examples-1-the-basics/
The above linked shows what im trying to achieve with the searchbar and populating the TableView.
Ive tried testing it with RxBlocking but my tests all seem to hang.
systemUnderTest is the viewModel
results is the Observable<[T]> that comes back from the service.
let results = systemUnderTest.results.toBlocking()
let noneObservableList = try! results.single()
//Then
XCTAssert(noneObservableList?.count == expectedCount)
It hangs on the try! results.single() and never hits the assert. Anyone know how to test this.
Thanks in advance.
This is systemUnderTest:
public class SearchViewModel: SearchViewModelContract {
public var query: Variable<String?> = Variable(String.EmptyString())
public var results: Observable<[ThirdPartySite]>
let minimumCharacterCount = 4
let dueTime = 0.3
let disposeBag = DisposeBag()
public init() {
results = Observable.just([Object]())
results = query.asObservable().throttle(dueTime, scheduler: MainScheduler.instance).flatMapLatest{
queryString -> Observable<Object> in
if let queryString = queryString {
if queryString.characters.count >= self.minimumCharacterCount {
return self.Something(siteName: queryString)
}
return Observable.just(Object(in: Object()))
}
return Observable.just(Object(in: Object()))
}.map { results in
return results.items
}.catchErrorJustReturn([Object]()).shareReplay(1)
}
}
I have some suggestions:
query and results should both be lets, not vars. You should never reset an Observable type. This is part of what it means to be functional.
you seem to be using a old version of RxSwift; I suggest you upgrade. - EDIT: DOH! Of course it's an old version of RxSwift, this is an old question!
unit testing code that has side effects (the network call) embedded in it can be a huge PITA. Pull up the side effects to a higher level so you can unit test this without it.
The debounce operator is a much better fit than throttle for data emissions. The latter works better for triggers.
As for your main question on how to unit test a search, I have found a lot of success with RxTest. Here is code for generating a searchTerm Observable along with a test to prove it works:
extension ObservableType where Element == String? {
func searchTerm(minCharacterCount: Int = 4, dueTime: RxTimeInterval = .milliseconds(300), scheduler: SchedulerType = MainScheduler.instance) -> Observable<String> {
return self
.compactMap { $0 }
.filter { minCharacterCount <= $0.count }
.debounce(dueTime, scheduler: scheduler)
}
}
class Tests: XCTestCase {
var scheduler: TestScheduler!
var result: TestableObserver<String>!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0, resolution: 0.001)
result = scheduler.createObserver(String.self)
disposeBag = DisposeBag()
}
func testExample() {
let input = scheduler.createColdObservable([
.next(1000, Optional.some("")),
.next(2000, Optional.some("xyz")),
.next(3000, Optional.some("wxyz")),
.next(4000, Optional.some("vwxyz")),
.next(4300, Optional.some("uvwxyz"))
])
input
.searchTerm(scheduler: scheduler)
.subscribe(result)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(result.events, [.next(3300, "wxyz"), .next(4600, "uvwxyz")])
}
}
The flatMapLatest should go in your side-effecting code, the stuff you don't unit test.

How to modify a struct with async callbacks?

I'm trying to update a struct with multi-level nested async callback, Since each level callback provides info for next batch of requests till everything is done. It's like a tree structure. And each time I can only get to one level below.
However, the first attempt with inout parameter failed. I now learned the reason, thanks to great answers here:
Inout parameter in async callback does not work as expected
My quest is still there to be solved. The only way I can think of is to store the value to a local file or persistent store and modify it directly each time. And after writing the sample code, I think a global var can help me out on this as well. But I guess the best way is to have a struct instance for this job. And for each round of requests, I store info for this round in one place to avoid the mess created by different rounds working on the same time.
With sample code below, only the global var update works. And I believe the reason the other two fail is the same as the question I mentioned above.
func testThis() {
var d = Data()
d.getData()
}
let uriBase = "https://hacker-news.firebaseio.com/v0/"
let u: [String] = ["bane", "LiweiZ", "rdtsc", "ssivark", "sparkzilla", "Wogef"]
var successfulRequestCounter = 0
struct A {}
struct Data {
var dataOkRequestCounter = 0
var dataArray = [A]()
mutating func getData() {
for s in u {
let p = uriBase + "user/" + s + ".json"
getAnApiData(p)
}
}
mutating func getAnApiData(path: String) {
var req = NSURLRequest(URL: NSURL(string: path)!)
var config = NSURLSessionConfiguration.ephemeralSessionConfiguration()
var session = NSURLSession(configuration: config)
println("p: \(path)")
var task = session.dataTaskWithRequest(req) {
(data: NSData!, res: NSURLResponse!, err: NSError!) in
if let e = err {
// Handle error
} else if let d = data {
// Successfully got data. Based on this data, I need to further get more data by sending requests accordingly.
self.handleSuccessfulResponse()
}
}
task.resume()
}
mutating func handleSuccessfulResponse() {
println("successfulRequestCounter before: \(successfulRequestCounter)")
successfulRequestCounter++
println("successfulRequestCounter after: \(successfulRequestCounter)")
println("dataOkRequestCounter before: \(dataOkRequestCounter)")
dataOkRequestCounter++
println("dataOkRequestCounter after: \(dataOkRequestCounter)")
println("dataArray count before: \(dataArray.count)")
dataArray.append(A())
println("dataArray count after: \(dataArray.count)")
if successfulRequestCounter == 6 {
println("Proceeded")
getData()
}
}
}
func getAllApiData() {
for s in u {
let p = uriBase + "user/" + s + ".json"
getOneApiData(p)
}
}
Well, in my actual project, I successfully append a var in the struct in the first batch of callbacks and it failed in the second one. But I failed to make it work in the sample code. I tried many times so that it took me so long to update my question with sample code. Anyway, I think the main issue is to learn appropriate approach for this task. So I just put it aside for now.
I guess there is no way to do it with closure, given how closure works. But still want to ask and learn the best way.
Thanks.
What I did was use an inout NSMutableDictionary.
func myAsyncFunc(inout result: NSMutableDictionary){
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let intValue = result.valueForKey("intValue")
if intValue as! Int > 0 {
//Do Work
}
}
dispatch_async(dispatch_get_main_queue()) {
result.setValue(0, forKey: "intValue")
}
}
I know you already tried using inout, but NSMutableDictionary worked for me when no other object did.