CAShapeLayer path animation has serious delay - ios

I'm attempting to create a loading screen that draws a circle while my app makes network requests. The amount drawn of the circle is meant to represent how close the request is to finishing. However there is a serious delay (~8 seconds) between the network request and the animation. After A LOT of searching I haven't found anyone that has had this problem before so I'm very desperate.
My setup right now is that an NSProgress object will be updated as a request is being made and will trigger a KVO Notification with the NSProgress object in the userInfo. This is the same method found here.
#Client.m
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"fractionCompleted"] && [object isKindOfClass:[NSProgress class]]) {
NSProgress *progress = (NSProgress *)object;
NSDictionary *userInfo = #{#"progress":progress};
[[NSNotificationCenter defaultCenter] postNotificationName:#"FractionCompleted" object:self userInfo:userInfo];
}
}
Then the view controller that is listening for the notification will update the LoadingProgressView with the fractionCompleted of the NSProgress object that it receives.
#MainViewController.m
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateProgress:) name:#"FractionCompleted" object:nil];
...
...
- (void)updateProgress:(NSNotification*)note {
NSProgress* prog = note.userInfo[#"progress"];
[self.progressIndicatorView updateProgress:(CGFloat)prog.fractionCompleted];
}
Now within the LoadingProgressView, the CAShapeLayer's strokeEnd property is set as the fractionCompleted value. I am using the same idea from the tutorial found here.
#LoadingProgressView.m
- (void)updateProgress:(CGFloat)frac {
_circlePathLayer.strokeEnd = frac;
}
When I actually make a request, nothing happens until about 5 seconds AFTER the request is finished. At that point the entire circle is animated at once.
I have ABSOLUTELY NO IDEA why this is happening and it's driving me crazy. I can clearly see using the debugger that the strokeEnd property is being updated in real-time and yet the LoadingProgressView refuses to re-render until much later. Any help is very much appreciated. Thanks.
EDIT: OK so a temporary solution was to fork a new thread with a delay of 0 to update the progress view for each notification. However this seems like poor thread management since I could be creating over a hundred different threads to do the same task. I'm wondering if there's anything else I can do.
- (void)updateProgress:(NSNotification*)note {
NSProgress* prog = note.userInfo[#"progress"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.progressIndicatorView updateProgress:(CGFloat)prog.fractionCompleted];
});
}

I ran into this same problem. It seems to be caused by setting the path from a background thread. Ensuring this happens on the main thread solved the problem. Now updating the shape draws immediately.

Related

Updating UITableView after download is finished/started

I'm dealing with a tricky situation in my app. This app shows a list of files in a UITableView, from there you can download (I use AFnetworking 2.0) a file and then you can see download progress in another UITableView (all the views are in a UITabBarController).
My problem is that I'm not really sure about how to reload the UITableVIew showing current downloads when a new one is added or when one is finished or cancelled. I tried using KVO but it didn't work observing the operation queue from AFnetworking.
EDIT:
This is the KVO code I tried
In my downloads UITableViewCell
- (void)viewDidLoad
{
[super viewDidLoad];
[[[[PodcastManager sharedManager] sessionManager] operationQueue] addObserver:self
forKeyPath:NSStringFromSelector(#selector(operations))
options:NSKeyValueObservingOptionNew
context:nil];
}
and then...
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == [[[PodcastManager sharedManager] sessionManager] operationQueue]) {
if ([keyPath isEqualToString:NSStringFromSelector(#selector(operations))]) {
NSLog(#"OperationCount: %lu", (unsigned long)[[[[[PodcastManager sharedManager] sessionManager] operationQueue] operations] count] );
}
}
}
But this "solution" didn't work here.
In the past, I faced a similar situation with another app and then I used blocks, but this time, I want to avoid that solution because there is some part of my current app's code I want to reuse in the future.
Anybody has faced a similar situation can bring some light?
Thank you.
Why not just run
[tableView reloadData];
after each download? Maybe I"m missing something. But that's what I'd do.

Smooth scrolling of text (Autoscroll) in UITextView iOS

I am working on a project where I there is some text in UItextview. The app wants to continuous smooth scroll that text and also wants to manage its scrolling speed. I mean here the text should scroll smoothly and the app contains slider where I can manage the speed.
Below is some sample code which I am using.
- (void) autoscrollTimerFired:(NSTimer *)timer {
[self.completeText setContentOffset:CGPointMake(0, self.completeText.contentOffset.y + 1.0) animated:NO];
if (self.completeText.contentOffset.y != self.completeText.contentSize.height - self.completeText.frame.size.height) {
scrollingTimer = [NSTimer scheduledTimerWithTimeInterval:velocityFactor target:self selector:#selector(autoscrollTimerFired:) userInfo:nil repeats:NO];
} else {
[scrollingTimer invalidate];
}
}
The Velocity factor is the number of seconds which ranges between 0.0 to 2.5.
It works nice in simulator but in device it moves with jerks or I must say like pausing at after some line.
Could you please suggest any solution here? All suggestions are welcome.
The NSTimers actually just periodically fire events into the enclosing NSRunLoop, which each thread has (or should have). So, if you have a child (or background) process running in a different thread, the NSTimers will fire against that thread's NSRunLoop instead of the application's main NSRunLoop.
NSTimer events fire on the thread where you scheduled the timer. If you scheduled the timer on the main run loop, then the timer will fire on the main thread and be “safe and synced” with input events.
So I have some suggestions for try that (I am not sure how much it will be successful in your case)
Don't use NSTimer. Try to call from main thread via selector with "afterDelay". As given in code
[self performSelector:#selector(autoScroll) withObject:nil afterDelay:1.0];
Use KVO or may be NSNotification for event triggering. (Not do it directly ,I Think better approach.)
I prefers KVO , so writing here the steps to use it :-
Make a class with a variable of NSNumber (which should accessible outside the class). Make a object of that class and add a Observer on it. (here model is object of that class).
[model addObserver:self forKeyPath:#"name" options:(NSKeyValueObservingOptionOld |
NSKeyValueObservingOptionNew) context:nil];
Implements it's delegate methods.
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// Do here what ever you want to do. It will call every time whenever there will be any change in that class variable.
}
Make a method in controller "autoScroll" which called via afterDelay selector. And change the value of the NSNumber vairabe value (any logic incremental way. Doesn't affect a lot).
Hope this helps you !!! Try This ...best of luck

After unregistering, I get "class deallocated while key value observers were still registered with it"

I have some code which applies to a number of objects, registering my class as the KVO:
for (SPPanelManager *manager in self.panelManagers) {
[manager addObserver:self forKeyPath:#"dataFetchComplete" options:0 context:NULL];
[manager fetchData];
}
Then when it observes a change, which happens on every of these objects, I un-register:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"dataFetchComplete"] && ((SPPanelManager *)object).dataFetchComplete) {
[object removeObserver:self forKeyPath:#"dataFetchComplete"];
//Other stuff
}
}
Then when I leave the UIViewController later, I get these errors for each of the manager objects:
An instance of class was deallocated while key value observers were
still registered with it. Observation info was leaked, and may even
become mistakenly attached to some other object.
I'm not sure why it's giving me this error - these are the only 2 places that KVO is ever referenced so it's not another observer.
Your class(observer) is being deallocated during some activity. You must unregister it before it is deallocated or not is in further use. Use code below in viewDidUnload: or dealloc:
for (SPPanelManager *manager in self.panelManagers) {
[manager removeObserver:self forKeyPath:#"dataFetchComplete" context:NULL];
}
Don't try to add or remove observers in observeValueForKeyPath:ofObject:change:context:. KVO expects the list of observers for a given Tuple(object, keyPath, context) to remain the same across a notification for that combination. Even it it works "sometimes" the behavior is non-deterministic because the order in which observers are notified is not guaranteed (it probably uses a set-type data structure internally.)
The simplest way around this problem might look something like:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"dataFetchComplete"] && ((SPPanelManager *)object).dataFetchComplete) {
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
[object removeObserver:self forKeyPath:#"dataFetchComplete"];
});
}
}
This will cause the observation to be removed at the next possible point in the run loop (barring custom run loop modes, which might delay its execution a bit, but usually won't be a problem). Also, you shouldn't need to worry about either self or object getting deallocated because they will be retained by the block closure until the block has executed and is, itself, releases.
As you've discovered, KVO isn't a particularly great API for one-shot notifications.
As to the initial error message, you'll need to remove the observation. You can probably get away with doing this in the observing object's dealloc but you should really avoid doing "real work" in dealloc and removing observations is arguably "real work." Unfortunately, there's not a standard teardown pattern in Cocoa, so you'd have to trigger the teardown yourself, perhaps when you segue out of the view controller, or something like that. Another answer suggested viewDidUnload but that is deprecated in iOS 6 and will never be called, so it's not a good approach any more.
You've to simply dealloc your GMS_MapView Object and as well as remove the MapView Observer forkeypath.
(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[objGMS_MapView removeObserver:self forKeyPath:#"myLocation"];
//=> Set map delegate to nil (to avoid: mapView:regionDidChangeAnimated:]: message sent to deallocated instance )
objGMS_MapView.delegate = nil;
objGMS_MapView = nil;
}

Previously posted solution to getting Notification (on NSOperationQueue finishing all tasks) is not working for me

Get notification when NSOperationQueue finishes all tasks
I have the same issue as the one posted by #porneL in the post above. I tried the solution posted by #NickForge (one that received 57 votes), but I am obviously doing it wrong because it does not work for me. Here is the problem setup and my implementation:
I need to start a spinner before kicking off a set of web-service operations, and stop the spinner when they are complete. The webservices are invoked through a shared AFHTTPClient instance (part of the AFNetworking package) which adds them to its NSOperationQueue.
I set up an observer in the ViewController from which the data loads are kicked off. Did this using the answer from the above post. Implementation in my VC looks like:
In my ViewController's init method:
//add WelcomeVC as an observer for AFHTTPClient dataloadOps notifications
[[[MyCustomAFHTTPClient sharedClient] operationQueue] addObserver:self forKeyPath:#"DataLoaderEvent" options:0 context:NULL];
In my ViewController's observeValueForKeyPath method:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if( (object == [[MyCustomAFHTTPClient sharedClient] operationQueue])
&& [keyPath isEqualToString:#"DataLoaderEvent"]) {
if ( [[[MyCustomAFHTTPClient sharedClient] operationQueue] operationCount] == 0) {
NSLog(#"EUREKA!!! QUEUE IS EMPTY! DATALOAD HAS COMPLETED!");
}
}
}
The ViewController's observeValueForKeyPath method however never gets called!
Any help to get this working would be most appreciated so I can then complete implementing the spinner.
Does operationQueue have a property called DataLoaderEvent? Normally one monitors the "operationCount" property of an NSOperationQueue.
See http://developer.apple.com/library/ios/#documentation/Cocoa/Reference/NSOperationQueue_class/Reference/Reference.html#//apple_ref/doc/uid/TP40004592
"The NSOperationQueue class is key-value coding (KVC) and key-value observing (KVO) compliant. You can observe these properties as desired to control other parts of your application. The properties you can observe include the following:
operations - read-only property
operationCount - read-only property"
try this:
[operation setCompletionBlock: ^{
NSLog(#"Finished an image.");
}];

iOS delegate issues with KVO

I have a method which sends out a request and has a delegate responder. The method is being called by a KVO responder. The code works fine when I call it from viewDidLoad, but when I make the request from the KVO method, it doesn't trigger the delegate method.
here is my KVO responder:
//observe app delegates userdata to check for when user data is available or data is changed
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:#"userData"] )
{
//goto facebook logic
[self FBFriendSuggester];
}
}
and the method it calls:
-(void)FBFriendSuggester
{
NSDictionary * fb_credentials = nil;
if([prefs objectForKey:#"facebookCredentials"])
{
fb_credentials = [prefs objectForKey:#"facebookCredentials"];
if ([fb_credentials objectForKey:#"expiry"] && [fb_credentials objectForKey:#"fbtoken"]) {
NSLog(#"found stored Facebook credentials.");
ApplicationDelegate.facebook.accessToken = [fb_credentials objectForKey:#"fbtoken"];
ApplicationDelegate.facebook.expirationDate = [fb_credentials objectForKey:#"expiry"];
}
}
if ([ApplicationDelegate.facebook isSessionValid]) {
printf("_valid facebook\n");
[ApplicationDelegate.facebook requestWithGraphPath:#"me/friends" andDelegate:self];
}else
printf("_invalid facebook\n");
}
and the delegate method:
- (void)request:(FBRequest *)request didLoad:(id)result{
NSLog(#"result2:%#",result);
NSDictionary * rawObject = result;
NSArray * dataArray = [rawObject objectForKey:#"data"];
for (NSDictionary * f in dataArray) {
[friends addObject:[[KNSelectorItem alloc] initWithDisplayValue:[f objectForKey:#"name"]
selectValue:[f objectForKey:#"id"]
imageUrl:[NSString stringWithFormat:#"http://graph.facebook.com/%#/picture?type=square", [f objectForKey:#"id"]]]];
}
[friends sortUsingSelector:#selector(compareByDisplayValue:)];
[self pickerPopup:self];
}
the delegate method doesn't get called in this scenario, but it does work fine when i call -(void)FBFriendSuggester directly from the controller. I don't know what to do, i tried setting a notification to respond in hopes that it would trigger the delegate, but that didn't work either.
I know this is rather old, but to give a reason for this happening, KVO notifications are sent on the thread that the change occurs on. So if a background thread makes a change to the property on the AppDelegate, the notification change will occur on the background thread as well.
So, in the context of your code, your -(void)FBFriendSuggester is most likely getting called in a background thread. You could confirm this by tossing a breakpoint in the code and looking at the Debug Navigator.
I'll continue with the assumption that this was true (It was running in a background thread).
This call, [ApplicationDelegate.facebook requestWithGraphPath:#"me/friends" andDelegate:self]; most likely needs to occur on the main thread. Reason for this is probably the underlying calls are using an NSURLConnection to make the call. The connections are made on background threads, but all delegate calls MUST be made back on the main thread, NOT a background thread. With the call andDelegate:self, its registering the object as a delegate on the background thread, instead of the main thread.
This is why your current solution fixes that issue.
Happy Coding!
I was able to work around my problem by issuing:
[self performSelectorOnMainThread:#selector(FBFriendSuggester) withObject:nil waitUntilDone:NO];
It would be great to know why this works like this though.

Resources