RxSwift: How to use shareReplay to lazily get subscription - ios

So I want to be able to lazily subscribe to shared data without it persisting when nobody is subscribed. Then if someone subscribes again, a new observable will be created. I would use a Variable, but I don’t want it to persist if no one is subscribed (because if I were using arrays or something larger than an int I don’t want to keep them in memory). My current implementation works, except when resubscribing it still gets the last value, which means the value is still persisted. I’m thinking of setting the observable to nil, but I don’t know where to do that. Can anyone help me complete this? The code below shows it mostly working, but it looks like the data is sticking around when no one is subscribed.
var switchTwoDisposable: Disposable? = nil
​
#IBAction func switchOneChanged(sender: UISwitch) {
if sender.on {
self.switchOneDisposable = currentNumber().subscribeNext { (value) in
log.debug("Switch 1: \(value)")
}
} else {
switchOneDisposable?.dispose()
}
}
​
#IBAction func switchTwoChanged(sender: UISwitch) {
if sender.on {
self.switchTwoDisposable = currentNumber().subscribeNext { (value) in
log.debug("Switch 2: \(value)")
}
} else {
switchTwoDisposable?.dispose()
}
}
​
var numberObservable: Observable<Int>? = nil
​
func currentNumber() -> Observable<Int> {
if let number = numberObservable {
return number
}
self.numberObservable = Observable<Int>.interval(5.0, scheduler: MainScheduler.instance).shareReplay(1)
return self.numberObservable!
}
​
​
// Switch 1 turned on
// logs "Switch 1: 0"
// logs "Switch 1: 1"
// Switch 2 turned on
// immediately logs "Switch 2: 1"
// logs "Switch 1: 2"
// logs "Switch 2: 2"
// Switch 1 turned off
// logs "Switch 2: 3"
// Switch 2 turned off
// nothing happens here until we take action again
// Switch 1 turned on
// logs "Switch 1: 3"
// logs "Switch 1: 0"

I finally found the convenient method that does exactly what I need. shareReplayLatestWhileConnected() on an observable will replay the latest value to the 2nd, 3rd, 4th, etc. subscribers, but when everyone unsubscribes, then the last value is not retained.
From the example above replace this line:
self.numberObservable = Observable<Int>.interval(5.0, scheduler: MainScheduler.instance).shareReplay(1)
...with this line:
self.numberObservable = Observable<Int>.interval(5.0, scheduler: MainScheduler.instance).shareReplayLatestWhileConnected()
Update
In my case, I specifically want to get a value from disk (e.g. Core data or NSUserDefaults) and then if someone updates that value, they can post to a notification that I'll observe with rx_notification. Therefore, in order for this lazy loading to truly work, I would also want an initial value. Therefore it would be helpful to use startWith in this case, where the value given to startWith is the current value on disk. So the code would be analogous to:
Observable<Int>.interval(5.0, scheduler: MainScheduler.instance).startWith(100).shareReplayLatestWhileConnected()

Related

firebase ios snapshot.childrenCount returns default 0 instead of database value

i've written this code:
func countTeacherSubstitutions() -> Int {
var count: Int = 0
database.observeSingleEvent(of: .value, with: { (snap) in
count = Int(snap.childrenCount)
print("\(snap.childrenCount) /c")
print("\(count) /t1")
})
print("\(count) /t2")
return count
}
The fuction should return 4 (childrens at root in database) instead of 0 (default firebase value)
print statements return the following:
"/c: 4"
"/t1: 4"
"/t2: 0"
a link to the github repo (this code isnt pushed yet): https://github.com/bcye/teacher-substitution-schedule-app-webscraper
Thanks for helping out!
The problem is this.
Firebase runs on another thread. Whats happening is its saying
Step 1) count = 0
Step 2) Ask firebase for the information, but, this could take some time so go to the next step and we will perform everything inside this brackets when we are done
Step 3) (Happens before stuff in step 2 is called) return Count (which is 0)
What you need to do is either directly update whatever you want to update with the value of count from inside of the firebase closure (where you print the value of c), or send in a completion handler to handle the value of count when it is done.
An example might be this
func countTeacherSubstitutions(completion: (Int) -> Void){
database.observeSingleEvent(of: .value, with: { (snap) in
count = Int(snap.childrenCount)
completion(count)
})
}
Call the above like so, from wherever.
let completion = { (count: Int) in
//I don't know what you do with count, but for this example ill update a label
label.text = "\(count)"
}
countTeacherSubstitutions(completion: completion)
What essentially happens here is you tell it the code you want to be run once its done doing what its doing, i.e getting data from the server. Without this in place, if it took 2 mins to get data from the server, you'd have to wait 2 mins to use your app.

Swift Firebase removal of observers not removing

I'm using Firebase listener to update state values for a remote camera. Once I have cycled through the camera lifecycle I want to remove the listeners so my camera does not start over and continue to take video.
Here is what I've done so far based on SO suggestions:
1) added FIRDatabaseHandle and called removeObserver(withHandle: handle) / no luck
2) simple called removeAllObservers() from the root reference to what you see below.
struct CameraActions {
let db = DataService.ds.db // this comes from a singleton used to for other Firebase calls
let uid = DataService.ds.curUser?.uid
var cameraRef:FIRDatabaseReference!
mutating func addCameraListener(cameraNum num:String, complete:#escaping(CameraStatus)->Void){
cameraRef = db.child("camera").child(num).child("status")
cameraRef.observe(.value, with: {
snap in
if let status = snap.value as? Int {
switch status {
case 0: complete(.ready)
case 2: complete(.isRecording)
case 4: complete(.hasStopped)
case 5: complete(.problem)
default: print("App is waiting on camera")
}
}
})
}
func cameraHasFinishedRecording(cameraNum num: String) {
cameraRef.removeAllObservers() // latest attempt here
db.child("camera").child(num).child("status").setValue(0) // this still triggers database call
}
Thanks in advance for any assistance.
Firebase works exactly as advertised. The removal of the observer was working but, another observer that should've been a single observer was firing. Thanks for the input and sorry for wasting your time.
Cheers!

NSFetchedResultsController with > now() doesn't refresh expired entries in swift

I have a UITableview I'm keeping updated with recent items. (That is, added to my CoreData within the last 5 minutes.) I have a field in my Item entity called 'expire_date' which is a Date type. When I download a new item in the background, I add it to CoreData, setting the expire_date to NSDate() plus 5 minutes:
item.expire_date = NSDate(timeIntervalSince1970: NSDate().timeIntervalSince1970+5*60);
My setup for the NSFetchedResultsController looks like:
let fetchRequest=NSFetchRequest(entityName: "Item")
fetchRequest.predicate = NSPredicate(format: "expire_date > now()")
let sortDescriptor = NSSortDescriptor(key: "expire_date")
fetchRequest.sortDescriptors=[sortDescriptor]
myFRC=NSFetchedResultsController(fetchRequest:fetchRequest, managedObjectContext:myMOC)
do {
try myFRC!.performFetch()
} catch {
print(error)
}
The NSFetchedResultsControllerDelegate code is the standard boilerplate for this kind of thing when using a UITableView.
This all works great when starting the app: only existing items that haven't 'timed out' show up. It also works great when new items are added.
The problem is that when an item in the list times out, it doesn't get removed from the results
Anyone have any idea how to accomplish this?
I had the possibility of one or more Items being selected in the UITableView, and I didn't want to lose those selections so I couldn't use W.K.S.'s answer unfortunately. It was strictly speaking correct from the information I had given, although it didn't update the list immediately after any item expires. However, Wain gave me the spark of an idea that worked the way I wanted.
It seems the Item is re-examined by the query when that item is updated, so the trick is to update the item in question when it has expired. I added an NSTimer variable:
var timeoutTimer:NSTimer?
I added a function, addMinimumTimeout() that (re)sets the timeout timer:
func addMinimumTimeout() {
if let _timeoutTimer=timeoutTimer {
_timeoutTimer.invalidate();
timeoutTimer=nil;
}
if myFRC?.fetchedObjects?.count==0 {
return;
}
if let firstItem = myFRC?.fetchedObjects?[0] as? Item {
let timeout=(firstItem.expire_date!.timeIntervalSince1970-NSDate().timeIntervalSince1970)+1.0;
timeoutTimer=NSTimer.scheduledTimerWithTimeInterval(timeout, target: self, selector: #selector(itemTimedOut), userInfo: firstItem, repeats: false);
}
}
My itemTimedOut code looks like this:
func itemTimedOut(timer:NSTimer) {
guard let item=timer.userInfo as? Item else {
return;
}
item.expire_date! = item.expire_date!;
do {
try item.managedObjectContext!.save();
} catch {
let saveError = error as NSError;
print("Error saving: \(saveError)")
return;
}
}
Then in my viewDidLoad, right after I perform my fetch, I call:
addMinimumTimeout();
and I also add it at the end of the boilerplate controller(controller, didChangeObject, atIndexPath, forChangeType, newIndexPath) function.
This way, if there's no items in the list, there's no timeout timer created, but as soon as one is added, the timeout timer is created. When an item is removed for whatever reason, the timer is updated, and if the last one is timed out or removed, there's no timeout timer running.

using AUScheduleParameterBlock from a host on AUAudioUnit v3

I am new AUAudioUnits and trying to get an understanding of how to use the v3 API. I am trying to schedule a parameter change on a kAudioUnitSubType_MultiChannelMixer AUAudioUnit so that I can ramp a gain change over time.
I am able to set the gain directly with a slider and the change takes affect immediately like this:
/*...*/
// Inside my ViewController
#IBAction func slider1Changed(sender: UISlider) {
player.gainParameter.setValue(sender.value, originator: nil)
}
// Inside my AudioPlayerClass
guard let parameterTree = self.multichannelMixer.parameterTree else {
print("Param Tree")
return
}
self.gainParameter = parameterTree.parameterWithID(
kMultiChannelMixerParam_Volume,
scope: kAudioUnitScope_Input,
element: 0)
but when I try to do this using the scheduleParameterBlock by adding this to the above AudioPlayerClass I would expect the gain to ramp from 1 to 0 over 10 seconds but there is no change:
let scheduleMixerParamBlock = self.multichannelMixer.scheduleParameterBlock
let rampTime = AUAudioFrameCount(10.secondsToSampleFrames)
scheduleMixerParamBlock(AUEventSampleTimeImmediate, rampTime, self.gainParameter.address, 0)
Examples I have seen of it working in the apple examples include code such as this (without the dispatch_async part):
parameterTree.implementorValueObserver = {param, value in
dispatch_async(dispatch_get_main_queue()) {
print(param, value)
}
scheduleMixerParamBlock(AUEventSampleTimeImmediate, rampTime, param.address, value)
}
When I run this and change the gain parameter with the slider then the block is run and the param and value are printed to the console with the correct looking values but the gain is not changed in the actual audio.
These examples I have seen are also on custom AUAudioUnits where the implementor has direct access to the dspKernel function so I might be missing something crucial there.
The other alternative I have to calculate a series of ramp values and then set the gain parameter directly for each value but since the scheduleParameterBlock is there is seems like I should be able to use that. Any help would be great. Thanks
You have to look at the AURenderEvent head in the internalRenderBlock which is operating on the audio render thread. Your scheduled parameter events will appear there for you to respond to.
For example, pass the head to this function:
void doEvents(AURenderEvent const* event)
{
while (event != nullptr) {
switch (event->head.eventType) {
case AURenderEventParameter:
doParameterEvent(event->parameter);
break;
default:
break;
}
event = event->head.next;
}
}
void doParameterEvent(AUParameterEvent const &event) {
switch (event.parameterAddress) {
case FilterParameterAddressFoo:
doFoo();
break;
case FilterParameterAddressBar:
doBar();
break;
default:
break;
}
}

Setting a subview.hidden = false locks up UI for many seconds

I'm using a button to populate a UIPickerView on a hidden UIVisualEffectView. The user clicks the button, the VisualEffectView blurs everything else, and the PickerView displays all the names in their contact list (I'm using SwiftAddressBook to do this.)
This works fine except when the user clicks the button, the UI locks up for about 5-10 seconds. I can't find any evidence of heavy CPU or memory usage. If I just print the sorted array to the console, it happens almost immediately. So something about showing the window is causing this bug.
#IBAction func getBffContacts(sender: AnyObject) {
swiftAddressBook?.requestAccessWithCompletion({ (success, error) -> Void in
if success {
if let people = swiftAddressBook?.allPeople {
self.pickerDataSource = [String]()
for person in people {
if (person.firstName != nil && person.lastName != nil) {
//println("\(person.firstName!) \(person.lastName!)")
self.pickerDataSource.append(person.firstName!)
}
}
//println(self.pickerDataSource)
println("done")
self.sortedNames = self.pickerDataSource.sorted { $0.localizedCaseInsensitiveCompare($1) == NSComparisonResult.OrderedAscending }
self.pickerView.reloadAllComponents()
self.blurView.hidden = false
}
}
else {
//no success, access denied. Optionally evaluate error
}
})
}
You have a threading issue. Read. The. Docs!
requestAccessWithCompletion is merely a wrapper for ABAddressBookRequestAccessWithCompletion. And what do we find there?
The completion handler is called on an arbitrary queue
So your code is running in the background. And you must never, never, never attempt to interact with the user interface on a background thread. All of your code is wrong. You need to step out to the main thread immediately at the start of the completion handler. If you don't, disaster awaits.

Resources