I'm implementing NSProgress support in a library, and I wrote some unit tests to test that everything's working correctly. While ideally I'd like to be able to pass some additional metadata (userInfo keys not used by NSProgress itself, but for users of my API to consume), for now I'm just trying to get localizedDescription and localizedAdditionalDescription to work like the documentation says they should. Since the method I'm testing extracts files from an archive, I set the kind to NSProgressKindFile and set the various keys associated with file operations (e.g. NSProgressFileCompletedCountKey).
I expect when I observe changes to localizedDescription with KVO, that I'll see updates like this:
Processing “Test File A.txt”
Processing “Test File B.jpg”
Processing “Test File C.m4a”
When I stop at a breakpoint and po the localizedDescription on the worker NSProgress instance (childProgress below), that is in fact what I see. But when my tests run, all they see is the following, implying it's not seeing any of the userInfo keys I set:
0% completed
0% completed
53% completed
100% completed
100% completed
It looks like the userInfo keys I set on a child NSProgress instance are not getting passed on to its parent, even though fractionCompleted does. Am I doing something wrong?
I give some abstract code snippets below, but you can also download the commit with these changes from GitHub. If you'd like to reproduce this behavior, run the -[ProgressReportingTests testProgressReporting_ExtractFiles_Description] and -[ProgressReportingTests testProgressReporting_ExtractFiles_AdditionalDescription] test cases.
In my test case class:
static void *ProgressContext = &ProgressContext;
...
- (void)testProgressReporting {
NSProgress *parentProgress = [NSProgress progressWithTotalUnitCount:1];
[parentProgress becomeCurrentWithPendingUnitCount:1];
[parentProgress addObserver:self
forKeyPath:NSStringFromSelector(#selector(localizedDescription))
options:NSKeyValueObservingOptionInitial
context:ProgressContext];
MyAPIClass *apiObject = // initialize
[apiObject doLongRunningThing];
[parentProgress resignCurrent];
[parentProgress removeObserver:self
forKeyPath:NSStringFromSelector(#selector(localizedDescription))];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
if (context == ProgressContext) {
// Should refer to parentProgress from above
NSProgress *notificationProgress = object;
[self.descriptionArray addObject:notificationProgress.localizedDescription];
}
}
Then, in my class under test:
- (void) doLongRunningThing {
...
NSProgress *childProgress = [NSProgress progressWithTotalUnitCount:/* bytes calculated above */];
progress.kind = NSProgressKindFile;
[childProgress setUserInfoObject:#0
forKey:NSProgressFileCompletedCountKey];
[childProgress setUserInfoObject:#(/*array count from above*/)
forKey:NSProgressFileTotalCountKey];
int counter = 0;
for /* Long-running loop */ {
[childProgress setUserInfoObject: // a file URL
forKey:NSProgressFileURLKey];
// Do stuff
[childProgress setUserInfoObject:#(++counter)
forKey:NSProgressFileCompletedCountKey];
childProgress.completedUnitCount += myIncrement;
}
}
At the time I increment childProgress.completedUnitCount, this is what the userInfo looks like in the debugger. The fields I set are all represented:
> po childProgress.userInfo
{
NSProgressFileCompletedCountKey = 2,
NSProgressFileTotalCountKey = 3,
NSProgressFileURLKey = "file:///...Test%20File%20B.jpg"; // chunk elided from URL
}
When each KVO notification comes back, this is how notificationProgress.userInfo looks:
> po notificationProgress.userInfo
{
}
I wanted to comment on #clarus's answer, but SO won't let me do readable formatting in a comment. TL;DR - their take has always been my understanding and it's something that bit me when I started working with NSProgress a few years back.
For stuff like this, I like to check the Swift Foundation code for implementation hints. It's maybe not 100% authoritative if stuff's not done yet, but I like seeing the general thinking.
If you look at the implementation of setUserInfoObject(: forKey:), you can see that the implementation simply sets the user info dict without propagating anything up to the parent.
Conversely, updates that impact the child's fraction completed explicitly call back to the (private) _parent property to indicate its state should update in response to a child change.
That private _updateChild(: from: to: portion:) only seems concerned updating the fraction completed and not anything related to the user info dictionary.
Ok, I had a chance to look at the code again with more coffee in my system and more time on my hands. I'm actually seeing it working.
In your testProgressReporting_ExtractFiles_AdditionalDescription method, I changed the code to this:
NSProgress *extractFilesProgress = [NSProgress progressWithTotalUnitCount:1];
[extractFilesProgress setUserInfoObject:#10 forKey:NSProgressEstimatedTimeRemainingKey];
[extractFilesProgress setUserInfoObject:#"Test" forKey:#"TestKey"];
And then in observeValueForKeyPath, I printed these objects:
po progress.userInfo {
NSProgressEstimatedTimeRemainingKey = 10;
TestKey = Test;
}
po progress.localizedAdditionalDescription
0 of 1 — About 10 seconds remaining
You can see the key-values I added, and the localizedAdditionalDescription was created based on those entries (notice the time remaining). So, this all looks like it's working correctly.
I think one point of confusion might be around the NSProgress properties and their effect on the key-values in the userInfo dict. Setting the properties doesn't add key-values to the userInfo dict, and setting the key-values doesn't set the properties. For example, setting the progress kind doesn't add the NSProgressFileOperationKindKey to the userInfo dict. The value in the userInfo dict, if present, is more of an override of the property that's only used when creating the localizedAdditionalDescription.
You can also see the custom key-value I added. So, this all looks like it's working right. Can you point me to something that still looks off?
Related
I am relatively new to swift and Firebase but I am definitely encountering a weird problem. What seems to be happening after messing around in the debugger is that the following function seems to be exhibiting weird behavior such as skipping the line storageRef.put()
So whats been happening is this, this function is triggered when the user clicks on a save button. As I observe in the debugger, storageRef is called but the if else statements are never invoked. Then, after my function returns the object which wasn't properly initalized, it then returns into the if else statement with the proper values... By then it is too late as the value returned and uploaded to the database is already incorrect..
func toAnyObject() -> [String : Any] {
beforeImageUrl = ""
let storageRef = FIRStorage.storage().reference().child("myImage.png")
let uploadData = UIImagePNGRepresentation(beforeImage!)
storageRef.put(uploadData!, metadata: nil) { (metadata, error) in
if (error != nil) {
print(error)
} else {
self.beforeImageUrl = (metadata?.downloadURL()?.absoluteString)!
print("upload complete: \(metadata?.downloadURL())")
}
}
let firebaseJobObject : [String: Any] = ["jobType" : jobType! as Any,
"jobDescription" : jobDescription! as Any,
"beforeImageUrl" : beforeImageUrl! as Any,]
return firebaseJobObject
}
Consider a change in your approach here. The button target-action is typical of a solution that requires an immediate response.
However, when you involve other processes (via networks) like the - (FIRStorageUploadTask *)putData:(NSData *)uploadData method above, then you must use some form of delegation to perform the delayed action whenever the server side method returns a value.
Keep in mind that when you are trying to use the above method, that it is not meant for use with large files. You should use - (FIRStorageUploadTask *)putFile:(NSURL *)fileURL method.
I'd suggest that you rework the solution to ensure that the follow-up action only happens when the put succeeds or fails. Keep in mind that network traffic means that the upload could take some time. If you want to validate that the put is completing with a success or failure, just add a breakpoint at the appropriate location inside the completion block and run the method on a device with/and without network access (to test both code flows).
I would like to make a function that only run once and cancel if is still running.
I tried it using a simple lock boolean on start/end, but sometimes it's "overlapping".
There's a better and secure way to do that?
#property (assign) BOOL lock;
- (void)myFuntion
{
if (self.lock) {
NSLog(#"(Canceled) Syncing is already running...");
return;
}
self.lock = YES;
// My Code
self.lock = NO;
}
The NSLock class should be able to help you here. I have not tried this example directly, but something like:
NSLock *myFunctionLock=[NSLock new]; // this should be a class data member/property/etc.
- (void)myFuntion
{
if (![myFunctionLock tryLock])
return; /* already running */
// My Synchronized Code
[myFunctionLock unlock];
}
We are all assuming that you are talking about concurrent programming, where you are running the same code on different threads. If that's not what you mean then you would need to explain what you DO mean, since code that runs on the same thread can only execute a function once at any particular moment.
Take a look at NSLock's tryLock function. The first caller to assert the lock gets back a TRUE, and can proceed to access the critical resource. Other callers get back FALSE and should not access the critical resource, but won't block.
I am trying to update a firebase node in transaction, simple stuff. Followed the doc:
https://www.firebase.com/docs/ios/guide/saving-data.html
Firebase* upvotesRef = [[Firebase alloc] initWithUrl: #"https://docs-examples.firebaseio.com/web/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes"];
[upvotesRef runTransactionBlock:^FTransactionResult *(FMutableData *currentData) {
NSNumber *value = currentData.value;
if (currentData.value == [NSNull null]) {
value = 0;
}
[currentData setValue:[NSNumber numberWithInt:(1 + [value intValue])]];
return [FTransactionResult successWithValue:currentData];
}];
The big question is:
How do I check the outcome of this transaction(success/fail)? I wish to make some UI changes depends on the outcome of it.
There is another method in SDK document appears to have a callback, but it did not explain which value should I check against. And it says something about the method could be run multiple times. How do I make sure when it gives the "final" result?
https://www.firebase.com/docs/ios-api/Classes/Firebase.html#//api/name/runTransactionBlock:andCompletionBlock:
Sorry I am such a beginner that apple style doc really doesn't come together without some examples.
If you just want to wait for the final value, runTransactionBlock:andCompletionBlock: is the method you want to look at. Here's some example code:
[upvotesRef runTransactionBlock:^FTransactionResult *(FMutableData *currentData) {
NSNumber *value = currentData.value;
if (currentData.value == [NSNull null]) {
value = 0;
}
[currentData setValue:[NSNumber numberWithInt:(1 + [value intValue])]];
return [FTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {
if (error) {
NSLog(#"Error: %#", error);
}
if (committed) {
NSLog(#"Data committed");
}
NSLog(#"Final Value: %#", snapshot.value);
}];
That last value there, snapshot.value is where you can get your final value. It's the same sort of FDataSnapshot that you get if you use observeEventType:withBlock:
If something goes wrong, you'll get an error.
If your data is committed, committed will be YES. If you returned [FTransactionResult abort] in your transaction block instead of [FTransactionResult successWithValue:], committed will be NO.
This means, if you read the counter at 4 and try to update it. You might try to update the counter at the same time someone else does. If yours get in first, snapshot.value will be 5. If the other person's update gets in before you do, the snapshot.value will be 6.
You probably wanted to get up to 6 no matter who upvoted first. To do this, you need to add an observer. The code for that might look like:
[upvotesRef observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
NSLog(#"New Value: %#", snapshot.value);
}];
With this, you don't need the completion block to find out the final value because every time the transaction blocks, the observer block will fire. In the example scenario above, it would fire once for 5 and once for 6 no matter who upvoted first. You do need the completion block if you want to find out whether or not your particular transaction succeeded and not just what value is at that location now.
And, just to be complete, there is one more method called runTransactionBlock:andCompletionBlock:withLocalEvents:. If others are also writing to the same location, the transaction block may run multiple times. This happens if it finds out that it is running on stale data. When it runs successfully on fresh data, it will call the completion block. However, you'll find that each time it runs, it will fire any observer blocks at that location. If you don't want this to happen, you should pass NO to withLocalEvents:. Your Firebase will trigger events at that location whenever a confirmed write goes through, but your local transaction's temporary writes, which are unconfirmed, will not.
Looking back to the example where you and another person are trying to upvote at the same time. By default, the observer will fire as soon as you try to update the count from 4 to 5. The actual transaction might fail because someone else pushed the upvote count from 4 to 5 at the same time. Your transaction block would then run again with the new data, 5, and see that it should push the count to 6. With local events set to NO, the observer will fire after the server lets you know someone else pushed the upvote count from 4 to 5, and not when you try to update the count from 4 to 5.
This isn't a huge deal with something simple like upvotes where everyone is just incrementing, but if you could potentially be pushing different data from other users, any observers at the location may see the data jump around before finally settling.
I want to make live streaming for more then two users on different devices and get api from opentok i had download demo app from ( https://github.com/opentok/OpenTok-iOS-Hello-World) and this is not webrtc, i had run application with key, session and token with disables of pear to pear,
And its working fine for two live streaming but while i tray to connect third stream i am not able to getting that,
I found staring in demo app that (On iPad 2 / 3 / 4, the limit is four streams. An app can have up to four simultaneous subscribers, or one publisher and up to three subscribers.)
with this i am testing with three iPads and got just two on screen
so how to make this more then two stream at a time in three iPads
The project you linked (OpenTok-iOS-Hello-World) is built to just subscribe to one stream. Just as a proof of concept, you can get two subscribers on screen pretty simply by just modifying a few methods and adding an instance variable in ViewController.m
Create a variable that tracks the number of subscribers:
#implementation ViewController {
OTSession* _session;
OTPublisher* _publisher;
OTSubscriber* _subscriber;
int _numSubscribers; // **NEW**
}
Initialize the variable in the initialization method:
- (void)viewDidLoad
{
[super viewDidLoad];
_session = [[OTSession alloc] initWithSessionId:kSessionId
delegate:self];
_numSubscribers = 0; // **NEW**
[self doConnect];
}
Make sure we aren't subscribing to our own stream:
static bool subscribeToSelf = NO;
Modify stop caring about whether there is already a subscriber in this session delegate method:
- (void)session:(OTSession*)mySession didReceiveStream:(OTStream*)stream
{
NSLog(#"session didReceiveStream (%#)", stream.streamId);
// See the declaration of subscribeToSelf above.
if ( (subscribeToSelf && [stream.connection.connectionId isEqualToString: _session.connection.connectionId])
||
(!subscribeToSelf && ![stream.connection.connectionId isEqualToString: _session.connection.connectionId])
) {
// ** Changing if statement **
if (_numSubscribers < 2) {
_subscriber = [[OTSubscriber alloc] initWithStream:stream delegate:self];
_numSubscribers++;
}
}
}
Place the subscribers next to one another, taking up a little less width:
- (void)subscriberDidConnectToStream:(OTSubscriber*)subscriber
{
NSLog(#"subscriberDidConnectToStream (%#)", subscriber.stream.connection.connectionId);
// ** Calculate the frame **
CGRect subFrame = CGRectMake(0, widgetHeight, widgetWidth / 2, widgetHeight)
if (_numSubscribers == 2) subFrame = CGRectOffset(subFrame, widgetWidth / 2, 0);
[subscriber.view setFrame:subFrame];
[self.view addSubview:subscriber.view];
}
NOTE: This solution doesn't result in a stable App. It should get you to a point where you can see both subscribers as long as you don't disconnect any of the iPads in between. To finish this off, you will need to store the OTSubscribers created in session:didRecieveStream: in a collection like NSArray, handle removing the right subscriber(s) and decrementing the _numSubscribers in session:didDropStream:, and thinking about how you want the updateSubscriber method to work instead.
If you look at the source code for hello world in the viewcontroller file line 93, you will see that it is only creating one subscriber. To have multiple subscribers simple create an array or hash object to store multiple subscribers.
This has been a hard one to search.
I found a similar question, iOS 5 Wait for delegate to finish before populating a table?, but the accepted answer was 'Refresh the table view,' and that does not help me. The other results I found tended to be in c#.
I have an app that streams from iPhone to Wowza servers. When the user hits record, I generate a unique device id, then send it to a PHP script on the server that returns a JSON document with configuration settings (which includes the rtmp dump link).
The problem is, the delegate methods are asynchronous, but I need to get the config settings before the next lines of code in my - (IBAction)recordButtonPressed method, since that code is what sets the profile settings, and then records based on those settings.
I've realized I could make the NSURLConnection in -recordButtonPressed like I am currently, and then continue the setup code inside the delegate method connectionDidFinishLoading (or just encapsulate the setup and method call it from there) but that's sacrificing coherent design for functionality and that sucks.
Is there not some simple waitUntilDelegateIsFinished:(BOOL)nonAsyncFlag flag I can send to the delegator so I can have sequential operations that pull data from the web?
I've realized I could make the NSURLConnection in -recordButtonPressed like I am currently, and then continue the setup code inside the delegate method connectionDidFinishLoading (or just encapsulate the setup and method call it from there) but that's sacrificing coherent design for functionality and that sucks.
You have analyzed and understood the situation and you have described its possible solutions perfectly. I just don't agree with your conclusions. This kind of thing happens all the time:
- (void) doPart1 {
// do something here that will eventually cause part2 to be called
}
- (void) doPart2 {
}
You can play various games with invocations to make this more elegant and universal, but my advice would be, don't fight the framework, as what you're describing is exactly the nature of being asynchronous. (And do not use a synchronous request on the main thread, since that blocks the main thread, which is a no-no.)
Indeed, in an event-driven framework, the very notion "wait until" is anathema.
Why not to use synchronous request?
Wrap your asynchronous NSURLConnection request in a helper method which has a completion block as a parameter:
-(void) asyncDoSomething:(void(^)(id result)completionHandler ;
This method should be implemented in the NSURLConnectionDelegate. For details see the example implementation and comments below.
Elsewhere, in your action method:
Set the completion handler. The block will dispatch further on the main thread, and then perform anything appropriate to update the table data, unless the result was an error, in which case you should display an alert.
- (IBAction) recordButtonPressed
{
[someController asyncConnectionRequst:^(id result){
if (![result isKindOfClass:[NSError class]]) {
dispatch_async(dispatch_get_main_queue(), ^{
// We are on the main thread!
someController.tableData = result;
});
}
}];
}
The Implementation of the method asyncConnectionRequst: could work as follows: take the block and hold it in an ivar. When it is appropriate call it with the correct parameter. However, having blocks as ivars or properties will increase the risk to inadvertently introduce circular references.
But, there is a better way: a wrapper block will be immediately dispatched to a suspended serial dispatch queue - which is hold as an ivar. Since the queue is suspended, they will not execute any blocks. Only until after the queue will be resumed, the block executes. You resume the queue in your connectionDidFinish: and connectionDidFailWithError: (see below):
In your NSURLConnectionDelegate:
-(void) asyncConnectionRequst:(void(^)(id result)completionHandler
{
// Setup and start the connection:
self.connection = ...
if (!self.connection) {
NSError* error = [[NSError alloc] initWithDomain:#"Me"
code:-1234
userInfo:#{NSLocalizedDescriptionKey: #"Could not create NSURLConnection"}];
completionHandler(error);
});
return;
}
dispatch_suspend(self.handlerQueue); // a serial dispatch queue, now suspended
dispatch_async(self.handlerQueue, ^{
completionHandler(self.result);
});
[self.connection start];
}
Then in the NSURLConnectionDelegate, dispatch a the handler and resume the
handler queue:
- (void) connectionDidFinishLoading:(NSURLConnection*)connection {
self.result = self.responseData;
dispatch_resume(self.handlerQueue);
dispatch_release(_handlerQueue), _handlerQueue = NULL;
}
Likewise when an error occurred:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
self.result = error;
dispatch_resume(self.handlerQueue);
dispatch_release(_handlerQueue), _handlerQueue = NULL;
}
There are even better ways, which however involve a few more basic helper classes which deal with asynchronous architectures which at the end of the day make your async code look like it were synchronous:
-(void) doFourTasksInAChainWith:(id)input
{
// This runs completely asynchronous!
self.promise = [self asyncWith:input]
.then(^(id result1){return [self auth:result1]);}, nil)
.then(^(id result2){return [self fetch:result2];}, nil)
.then(^(id result3){return [self parse:result3];}, nil)
.then(^(id result){ self.tableView.data = result; return nil;}, ^id(NSError* error){ ... })
// later eventually, self.promise.get should contain the final result
}