RxSwift: UISwitch toggle back to initial State - ios

I am looking for solution for a problem I am facing. I have TableView which has multiple cell and each cell has a UISwitch and state of that switch (either on/off) is being set like this:
viewModel.permissions
.drive(tblMessageTypes.rx.items(cellIdentifier: "PermissionCellIdentifier", cellType: PermissionTableViewCell.self)) { [switchSubject] _, item, cell in
cell.backgroundColor = .clear
cell.lblTitle.text = item.permissionTitle
cell.lblDetail.text = item.permissionDescirption
cell.selectionStyle = .none
cell.switchPermission.isOn = item.permissionValue
cell.switchPermission.isEnabled = !item.isPermissionReadOnly
cell.switchPermission.rx.controlEvent(.valueChanged)
.withLatestFrom(cell.switchPermission.rx.value)
.map { isOn in
var newPermission = item
newPermission.permissionValue = isOn
return newPermission
}
.bind(to: switchSubject)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
So when Switch is toggled, I am passing an current cell value with update Switch state and based on that I am calling api in my VM like this:
let serverReponse = inputs.switchToggle
.map { permission in
let dicto = permission.toDictionary()
let parameters = ["permissions": [dicto]]
return parameters
} .flatMapLatest { parameters in
userService.updateUserPermission(parameters: parameters)
.trackActivity(inputs.indicator)
.materialize()
}
.share()
Now the issue I have is, if api is failed due to any reason, How should that UISwitch should fallback to initial state, i.e if it was Off and user toggled it to On State and Api was failed it should fall back to Off State.

I would expect to see something like the below. The things to note about this code:
All "permission" objects have some way of uniquely identifying each cell, I'm using UUID here, but you might already have an ID of some sort.
The "permissions" Observable in the ViewModel is used to update each individual cell. All the cells subscribe to this, map out their own Permission object and display it.
As you already have in your view controller, all cells send an updated permission through the switchToggle Observable.
The below code compiles and resets the cells as required. I'm relying on the zip operator to ensure that I am combining the permission that was sent to the request with the response from the server. If the server errors, the permissionValue in the appropriate object in the dictionary will be set to the opposite of what was sent to the server.
The more astute observers (pun intended) of the code below, you will notice that when the server request fails, the permissions object will emit but it won't actually change state since it was the switch that was toggled, not the permissionValue in the dictionary. However, that emission will get picked up by the switch which will cause it to reset itself.
struct Inputs {
let initialPermissions: [Permission]
let switchToggle: Observable<Permission>
}
struct ViewModel {
let permissions: Observable<[UUID: Permission]>
init(_ inputs: Inputs, userService: UserService) {
let serverReponse = inputs.switchToggle
.map { permission in
let dicto = permission.toDictionary()
let parameters = ["permissions": [dicto]]
return parameters
}
.flatMapLatest { parameters in
userService.updateUserPermission(parameters: parameters)
.materialize()
}
.share()
let perms = Dictionary(grouping: inputs.initialPermissions, by: { $0.id })
.mapValues { $0.first! }
permissions = Observable.zip(inputs.switchToggle, serverReponse)
.filter { $0.1.error != nil }
.map { Permission(id: $0.0.id, permissionValue: !$0.0.permissionValue) }
.scan(into: perms, accumulator: { state, update in
state[update.id] = update
})
.startWith(perms)
}
}

Related

I can't get array and put in the another array swift from Firebase storage

I want get folders and images from Firebase storage. On this code work all except one moment. I cant append array self.collectionImages in array self.collectionImagesArray. I don't have error but array self.collectionImagesArray is empty
class CollectionViewModel: ObservableObject {
#Published var collectionImagesArray: [[String]] = [[]]
#Published var collectionImages = [""]
init() {
var db = Firestore.firestore()
let storageRef = Storage.storage().reference().child("img")
storageRef.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for prefixName in result.prefixes {
let storageLocation = String(describing: prefixName)
let storageRefImg = Storage.storage().reference(forURL: storageLocation)
storageRefImg.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for item in result.items {
// List storage reference
let storageLocation = String(describing: item)
let gsReference = Storage.storage().reference(forURL: storageLocation)
// Fetch the download URL
gsReference.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
// Get the download URL for each item storage location
let img = "\(url?.absoluteString ?? "placeholder")"
self.collectionImages.append(img)
print("\(self.collectionImages)")
}
}
}
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
//
self.collectionImagesArray.append(self.collectionImages)
}
}
}
If i put self.collectionImagesArray.append(self.collectionImages) in closure its works but its not what i want
The problem is caused by the fact that calling downloadURL is an asynchronous operation, since it requires a call to the server. While that call is happening, your main code continues so that the user can continue to use the app. Then when the server returns a value, your closure/completion handler is invoked, which adds the URL to the array. So your print("\(self.collectionImagesArray)") happens before the self.collectionImages.append(img) has ever been called.
You can also see this in the order that the print statements occur in your output. You'll see the full, empty array first, and only then see the print("\(self.collectionImages)") outputs.
The solution for this problem is always the same: you need to make sure you only use the array after all the URLs have been added to it. There are many ways to do this, but a simple one is to check whether your array of URLs is the same length as result.items inside the callback:
...
self.collectionImages.append(img)
if self.collectionImages.count == result.items.count {
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
Also see:
How to wait till download from Firebase Storage is completed before executing a completion swift
Closure returning data before async work is done
Return image from asynchronous call
SwiftUI: View does not update after image changed asynchronous

Why are previous views that have been popped from the stack still active

I have this truck driving app in swiftUI where I use fire base to log users in and out. The problem is that when I sign in with one user, and all it’s fire base functionalities are triggered, After I log out from the current user and into a new user, the functionality of the old user is still playing out. I think it might have something to do with the firebase functions being in an onAppear method. I am not sure though.
This is the firebase code. I dont think what Im querying has any relation to the solution so I wont explain it but if you think it does than please let me know.
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]) { (_,_) in
}
myDrivers = []
getEmails()
db.collectionGroup("resources").whereField("control", isEqualTo: true).addSnapshotListener { (snapshot, err) in
if myDrivers.count == 0{}
else{
if err != nil{print("Error fetching motion status: \(err)")}
if ((snapshot?.documents.isEmpty) != nil){}
// Gets all the driverLocation documents
for doc in snapshot!.documents{
if myDrivers.contains(doc.reference.parent.parent!.documentID){
let isDriving = doc.get("isDriving") as! Bool
let isStopped = doc.get("isStopped") as! Bool
let notified = doc.get("notified") as! Bool
if (isDriving == false && isStopped == false) || notified{}
else{
// Gets the name of the drivers
doc.reference.parent.parent?.getDocument(completion: { (snapshot, err) in
if err != nil{print("Error parsing driver name: \(err)")}
let firstName = snapshot?.get("firstName") as! String
let lastName = snapshot?.get("lastName") as! String
self.notifiedDriver = "\(firstName) \(lastName)"
})
// Logic
if isDriving{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) is back on the road.")
showDrivingToast.toggle()
doc.reference.updateData(["notified" : true])
}else if isStopped{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) has stopped driving.")
showStoppedToast.toggle()
doc.reference.updateData(["notified" : true])
}
}
}
else{}
}
}
}
})
Listeners don't die when a view controller is left. They remain active.
It's important to manage them through handlers for specific listeners as a view closes or the user navigates away. Here's how to remove a specific listener
let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
// ...
}
and then later
// Stop listening to changes
listener.remove()
Or, if your user is logging out, you can use removeAllObservers (for the RealtimeDatabase) to remove them all at one time, noting that
removeAllObservers must be called again for each child reference where
a listener was established
For Firestore, store the listeners in a listener array class var and when you want to remove them all, just iterate over the array elements calling .remove() on each.
There's additional info in the Firebase Getting Started Guide Detach Listener

Get values from two apis into two different Observable and perform some operation

I have two independent observables. I need to perform some operation when both of them are complete and each of them provided an array.
let myObj1Array = myObj1Manager.getMyObj1List()//returns Observable<[MyObj1]>
let myObj2Array = myObj2Manager.getMyObj2List()//returns Observable<[MyObj2]>
Now I need to compare values of myObj1Array and myObj2Array and on the basis of that create another array using values from both arrays. I know how to subscribe 1 variable but not sure how to observe completion of two different arrays.
Edit:
I also tried following but I get values only from first array:
let myObj1Array = myObj1Manager.getMyObj1List()
let myObj2Array = myObj1Array.flatMap { _ in myObj2Manager.getMyObj2List() }
Observable.combineLatest(myObj1Array, myObj2Array)
.subscribe(onNext: { (sss, sds) in
print(sss)
})
.addDisposableTo(disposeBag)
I am actually kind of clueless about how to handle such scenario.
Edit2:
function to get the observables in first array:
func getMyObj1List() -> Observable<[MyObj1]> {
return Observable.create { observer -> Disposable in
self.specialsRest.getMyObj1List { response, error in
if let error = error {
observer.onError(Exception(error))
return
}
guard let saleItems = MyObj1.decode(data: response?.data) else {
observer.onError(Exception("Could not decode specials!"))
return
}
queueBackground.async {
observer.onNext(saleItems)
observer.onCompleted()
}
}
return Disposables.create { self.specialsRest.cancel() }
}
}
DispatchGroup is probably the way to go here.
https://developer.apple.com/documentation/dispatch/dispatchgroup
When all work items finish executing, the group executes its completion handler. You can also wait synchronously for all tasks in the group to finish executing.
var dg:DispatchGroup = DispatchGroup()
//Wherever you start your observables.
//Start Observer1
dg.enter()
//Start Observer2
dg.enter()
...
...
...
//Wherever you retrieve data
SomeAsyncFuncForObserver1 {
//Get Data
dg.leave()
}
SomeAsyncFuncForObserver2 {
//Get Data
dg.leave()
}
dg.notify(queue: .main) {
print("all finished.")
}
I believe you need to use zip instead of combineLatest. From the docs
The CombineLatest operator behaves in a similar way to Zip, but while
Zip emits items only when each of the zipped source Observables have
emitted a previously unzipped item, CombineLatest emits an item
whenever any of the source Observables emits an item (so long as each
of the source Observables has emitted at least one item).
Observable
.zip(myObj1Array, myObj2Array)
.subscribe(onNext: { (sss, sds) in
print(sss)
})
.addDisposableTo(disposeBag)

Rx: How to modify a shared source observable within a retry

Top Level Question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
Solution(s) I have considered:
The suggestion of using defer from this post doesn't seem to naturally port over if the source observable needs to be shared.
Use case (to fully elaborate the question)
Say I have a server connection object that, when initialized, connects to an url. Once it is created, I can also use it to get a data stream for a particular input.
class ServerConnection {
var url: URL
init(url: URL)
func getDataStream(input: String) -> Observable<Data> // the observable also errors when the instance is destroyed.
}
However, one particular url or another may be broken or overloaded. So I may want to obtain the address of a mirror and generate a new ServerConnection object. Let's say I have such a function.
// At any point in time, gets the mirror of the url with the lowest load
func getLowestLoadMirror(url: URL) -> URL {}
Ideally, I want this "mirror url" switching should be an implementation detail. The user of my code may only care about the data they receive. So we would want to encapsulate this logic in a new class:
class ServerConnectionWithMirrors {
private var currentConnection: BehaviorRelay<ServerConnection>
init(startingURL: URL)
func dataStream(for inputParams: String) -> Observable<Data>
}
// usage
let connection = ServerConnectionWithMirrors(startingURL: "www.example.com")
connection.dataStream(for: "channel1")
.subscribe { channel1Data in
// do something with channel1Data
}.disposed(by: disposeBag)
connection.dataStream(for: "channel2")
.subscribe { channel2Data in
// do something with channel2Data
}.disposed(by: disposeBag)
How should I write the dataStream() function for ServerConnectionWithMirrors? I should be using retries, but I need to ensure that the retries, when faced with a particular error (ServerOverLoadedError) update the value on the behaviorRelay.
Here is code that I have so far that demonstrates the crux at what I am trying to do. One problem is that multiple subscribers to the behaviorRelay may all update it in rapid succession when they get an error, where only one update would do.
func dataStream(for inputParams: String) -> Observable<Data> {
self.currentConnection.asObservable()
.flatMapLatest { server in
return server.getDataStream(input: inputParams)
}
.retryWhen { errors in
errors.flatMapLatest { error in
if error is ServerOverLoadedError {
self.currentConnection.accept(ServerConnection(url: getLowestLoadURL()))
} else {
return Observable.error(error)
}
}
}
}
The answer to your top level question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
You cannot modify a retry's source observable from within the retry. (full stop) You cannot do this whether it is shared or not. What you can do is make the source observable in such a way that it naturally updates its data for every subscription.
That is what the question you referred to is trying to explain.
func getData(from initialRequest: URLRequest) -> Observable<Data> {
return Observable.deferred {
var correctRequest = initialRequest
let correctURL = getLowestLoadMirror(url: initialRequest.url!)
correctRequest.url = correctURL
return Observable.just(correctRequest)
}
.flatMapLatest {
getDataFromServer(request: $0)
}
.retryWhen { error in
error
.do(onNext: {
guard $0 is ServerOverloadedError else { throw $0 }
})
}
}
With the above code, every time deferred is retried, it will call its closure and every time its closure is called, the URL will the lowest load will be used.

RxSwift, how do I chain different observables

I am still a beginner in Reactive programming, and RxSwift in general.
I want to chain two different operation. In my case I simply want to download a zip file from a web server, and then unzip it locally.
I also want, at the same time to show the progress of the downloaded files.
So I started creating the first observable:
class func rx_download(req:URLRequestConvertible, testId:String) -> Observable<Float> {
let destination:Request.DownloadFileDestination = ...
let obs:Observable<Float> = Observable.create { observer in
let request = Alamofire.download(req, destination: destination)
request.progress { _, totalBytesWritten, totalBytesExpectedToWrite in
if totalBytesExpectedToWrite > 0 {
observer.onNext(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
}
else {
observer.onNext(0)
}
}
request.response { _, response, _, error in
if let responseURL = response {
if responseURL.statusCode == 200 {
observer.onNext(1.0)
observer.onCompleted()
} else {
let error = NSError(domain: "error", code: responseURL.statusCode, userInfo: nil)
observer.onError(error)
}
} else {
let error = NSError(domain: "error", code: 500, userInfo: nil)
observer.onError(error)
}
}
return AnonymousDisposable () {
request.cancel()
}
}
return obs.retry(3)
}
After that, I create a similar function for the unzip
class func rx_unzip(testId:String) -> Observable<Float> {
return Observable.create { observer in
do {
try Zip.unzipFile(NSURL.archivePath(testId), destination: NSURL.resourceDirectory(testId), overwrite: true, password: nil)
{progress in
observer.onNext(Float(progress))
}
} catch let error {
observer.onError(error)
}
observer.onCompleted()
return NopDisposable.instance
}
}
For now I have this logic on the "View model layer", so I download-> subscribe on completed-> unzip
What I want is to combine the two Observable in one, in order to perform the download first, then on completed unzip the file. Is there any way to do this?
Concat operator requires the same data type
Indeed, the concat operator allows you to enforce the sequence of observables, however an issue you might encounter with using concat is that the concat operator requires that the Observables have the same generic type.
let numbers : Observable<Int> = Observable.from([1,2,3])
let moreNumbers : Observable<Int> = Observable.from([4,5,6])
let names : Observable<String> = Observable.from(["Jose Rizal", "Leonor Rivera"])
// This works
numbers.concat(moreNumbers)
// Compile error
numbers.concat(names)
FlatMap operator allows you to chain a sequence of Observables
Here's an example.
class Tag {
var tag: String = ""
init (tag: String) {
self.tag = tag
}
}
let getRequestReadHTML : Observable<String> = Observable
.just("<HTML><BODY>Hello world</BODY></HTML>")
func getTagsFromHtml(htmlBody: String) -> Observable<Tag> {
return Observable.create { obx in
// do parsing on htmlBody as necessary
obx.onNext(Tag(tag: "<HTML>"))
obx.onNext(Tag(tag: "<BODY>"))
obx.onNext(Tag(tag: "</BODY>"))
obx.onNext(Tag(tag: "</HTML>"))
obx.onCompleted()
return Disposables.create()
}
}
getRequestReadHTML
.flatMap{ getTagsFromHtml(htmlBody: $0) }
.subscribe (onNext: { e in
print(e.tag)
})
Notice how getRequestReadHTML is of type Observable<String> while the function getTagsFromHtml is of type Observable<Tag>.
Using multiple flatMaps can increase emission frequency
Be wary however, because the flatMap operator takes in an array (e.g. [1,2,3]) or a sequence (e.g. an Observable) and will emit all of the elements as an emission. This is why it is known to produce a transformation of 1...n.
If you defined an observable such as a network call and you are sure that there will only be one emission, you will not encounter any problems since its transformation is a 1...1 (i.e. one Observable to one NSData). Great!
However, if your Observable has multiple emissions, be very careful because chained flatMap operators will mean emissions will exponentially(?) increase.
A concrete example would be when the first observable emits 3 emissions, the flatMap operator transforms 1...n where n = 2, which means there are now a total of 6 emissions. Another flatMap operator could again transform 1...n where n = 2, which means there are now a total of 12 emissions. Double check if this is your expected behavior.
You can use concat operator to chain these two Observables. The resulting Observable will send next values from the first one, and when it completes, from the second one.
There is a caveat: you will get progress values ranging from 0.0 to 1.0 from rx_download and then again the progress from rx_unzip will start with 0.0. This might be confusing to the user if you want to show the progress on a single progress view.
A possible approach would be to show a label describing what is happening along with the progress view. You can map each Observable to a tuple containing the progress value and the description text and then use concat. It can look like that:
let mappedDownload = rx_download.map {
return ("Downloading", $0)
}
let mappedUnzip = rx_download.map {
return ("Unzipping", $0)
}
mapped1.concat(mapped2)
.subscribeNext({ (description, progress) in
//set progress and show description
})
Of course, there are many possible solutions, but this is more of a design problem than a coding one.

Resources