I used KVO to observe changes in a frame and set another frame accordingly
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:KeyPathForFrameBeingObserved]) {
[UIView animateWithDuration:1 animations:^{
CGRect frame = [self playWithFrame:viewBeingMonitored.frame];
self.viewToAnimate.frame = frame;
}];
}
}
Code in the animation block is executed but animation is not working, I suspected that repeated calls to animation method could cause this but using log messages I found that animation is not working even for a single call, can anyone explain that ?
iPad 2/(iOS8.4)
I have tried including the animation method call in dispatch_async(dispatch_get_main_queue(), ^{ ... }); but animation still not working.
I have tried pushing the observed changes in queue and delaying the animation method for a second using dispatch_after to run it only using the last element in the queue to avoid repeated calls but didn't work.
KVO is working perfectly (adding the observer is already handled), but the code in the animation block is executed without animation.
It seems like you're trying to observe the property of an UIKit object. Keep in mind that:
... the classes of the UIKit framework generally do not support KVO ...
Source: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/KVO.html
So, you have to check if your observeValueForKeyPath:ofObject:change:context: is calling in fact.
This is an older question but I thought I would answer in case someone else is reading this. You will need to call setFrame to set the frame of the animation instead of the simple assignment.
CGRect frame = [self playWithFrame:viewBeingMonitored.frame];
[self.viewToAnimate setFrame:frame];
Related
I am trying to implement a KVO example for clipsToBounds property of all subviews in my UIView. I do not quite understand how to change the value in observeValueForKeyPath method. I am using this code:
-(void)ViewDidLoad{
[self.navigationController.view addObserver:self forKeyPath:#"clipsToBounds" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"Triggered...")
}
It is triggered when ever i change the property clipToBounds of a subview that exists in the UIView i have. I need to change the value back to false for every trigger that happens. What should i write inside the observeValueForKeyPath to change the clipsToBounds property? Any help appreciated.
of course adding the Observer must be done before it works.
Guessing your typo in "ViewDidLoad" would just never be called because it should be "viewDidLoad".
Apart from that your KVO pattern could look like..
static void *kvoHelperClipsToBounds = &kvoHelperClipsToBounds;
-(void)viewDidLoad {
[self.navigationController.view addObserver:self forKeyPath:#"clipsToBounds" options:NSKeyValueObservingOptionNew context:&kvoHelperClipsToBounds];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == kvoHelperClipsToBounds) {
NSLog(#"context compared successful...");
//be careful what you cast to.. i dont check isKindOf here.
UINavigationBar* navbar = (UINavigationBar*)object;
if (navbar.subviews.count > 1) {
__kindof UIView *sub = navbar.subviews[1];
if (sub.clipsToBounds) {
dispatch_async(dispatch_get_main_queue(),^{
sub.clipsToBounds = NO;
[self.navigationItem.titleView layoutIfNeeded];
});
}
}
}
// or compare against the keyPath
else if ([keyPath isEqualToString:#"clipsToBounds"]) {
NSLog(#"classic Key compare Triggered...");
}
else
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
[super observeValueForKeyPath...] passes not recognized keyPath to super to let super's class KVO work, otherwise those would be ignored if super's implementation would rely on observing them. Which should also explain how you could observe all subviews if needed. But think about that there could be potentially hundrets of subviews triggering observeValueForKeyPath if a subclass of UIView would implement it and all subviews (or the ones you like) would be inherited also from this special subclass.
When you change clipsToBounds inside the KVO where you observe it, you possibly invoke a loop, specially when you watch both - old and new values. you would change the property, the property triggers kvo, kvo changes the property, the property triggers kvo and on and on.
set [self.navigationController.view setClipsToBounds:YES] to change the property. But if done inside KVO it will trigger KVO again as explained.
Usually you would set clipsToBounds in -initWithFrame: or in -initWithCoder: or via Interface Builder and maybe just observe if it gets changed to adapt some other code.
Sidenote: the context just needs to be unique to distinguish it from other KVO.. it could also be reference to a real objects pointer.
Don't forget added Observers must be removed before deallocation.
I need to find a way to create some class that is always alive in the app. This class doesn't know anything about the other classes in the project. It will be able to "follow" all UIViews on screen-so every moment I can check(loop) over the views and get a pointer to each of them.
It has to run live, and always check positions of views (is it a memory problem?)
Why a pointer? because I need to know everything about it, so if its some kind of moving animation, or maybe it has some meta data like tags, etc. so only knowing there is some view at a certain position is not enough.
Is it possible in iOS ?
The whole idea sounds like an antipattern. However, …
Simply traverse the view tree and add a KVO handler to every view for every interesting property.
- (void)traverseSubviewsOfParentView:(UIView*)view
for( UIView* subview in view.subviews )
{
[view addObserver:self forKeyPath:#"frame" options: NSKeyValueObservingOptionNew];
…
[self traverseSubviewsOfParentView:subview context:NULL];
}
Then implement the observation method:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if( [#"frame" isEqualToString:keyPath] )
{
// Do what you want to do
}
…
}
Additionally you have to observe the subviews property of every view to get notified, when a view is inserted or removed.
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.
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
I have some playing cards that flip over when you tap them. I want some stuff to happen after the flip animation is complete, so I have this in my UIView animation cycle:
[UIView setAnimationDidStopSelector:#selector(flipAnimationDone:finished:context:)];
...which calls this:
-(void)flipAnimationDone:(NSString *)animationID finished:(BOOL)finished context:(void *)context {
if (finished == YES) {
// other stuff here
}
}
Now, I need the if (finished == YES) bit in there, because otherwise the other stuff will fire even if the user taps the card again mid-animation, which is bad -- it needs to only happen if the flip animation completes completely :)
Problem is, this isn't working. If I have the if in there, the other stuff doesn't ever fire, no matter what. If I leave the if out, the other stuff fires, but possibly at the wrong time.
What am I doing wrong with the finished bit that is making it not work properly?
Thanks!
Notice that the documentation says the method signature is
- (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context;
Specifically,
finished
An NSNumber object containing a Boolean value. The value is YES if the animation ran to completion before it stopped or NO if it did not.
So, you should make it (NSNumber *)finished and if ([finished boolValue]) { ... }.