Detect movement of UIView inside another UIView - ios

What I have:
When I move a UIView I am detecting its movement by doing the following:
[myView.layer addObserver:self forKeyPath:#"position" options:NSKeyValueObservingOptionNew context:nil];
Being myView the UIView I am moving and self a class I have to detect the different positions the UIView has.
The Issue:
When I put myView inside another UIView and I move the anotherView:
[anotherView addSubview: myView];
The method:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
Is not called anymore, although in theory myView is moving as well. I tried to use NSNotification to be fired every time a "movement" occurred, but I find it clumsy. Is there an "elegant" solution for this kind of problem?
For the movement of the UIView I am using this methods:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

I have uploaded a sample project here that does this cleanly:
https://github.com/n9986/ObservingUIViewMovement
I had a similar requirement sometime back. The issue here is that when you add the UIView to another UIView (call it superView), it now resides in the coordinate space of superView. Therefore, the movement of superView in it's parent's coordinate space does not effect it's children.
I will explain the code here a bit.
We have ViewController, MyView and InsideView classes illustrating your typical classes. I want to observe in the viewController whether the InsideView has moved or not. So in the custom class, I added a property positionInWindow and updated it whenever the superView moved.
So in InsideView.m:
// Make sure this property exists in .h file to make the class KVC compliant
#synthesize positionInWindow;
// This method is called when the super view changes.
- (void)didMoveToSuperview
{
// Drop a random log message
NSLog(#"MAI SUPERVIEW HAS CHANGED!!!");
// Start observing superview's frame
[self addObserver:self
forKeyPath:#"superview.frame"
options: NSKeyValueObservingOptionNew
context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// Here we update the positionInWindow because
// we know the superView.frame has changed
CGPoint frameOrigin = self.frame.origin;
[self setPositionInWindow:[[self window] convertPoint:frameOrigin
fromView:self]];
}
And anywhere you want to monitor this view:
// On an instance of InsideView
[insideView addObserver:self
forKeyPath:#"positionInWindow"
options:NSKeyValueObservingOptionNew
context:nil];
The good part about this solution is that InsideView does not need to know who is it's superView. This code will work even if the superView is later changed. There is no modification to the MyView class. And any class can monitor it's property independent of that fact.

As far as I know, UIView's don't move "on their own", which means you probably have some code around there for the movement
Maybe try setting some delegate or notification there instead of using addObserver:

Related

KVO check for change of clipsToBounds of all subviews in an UIView in objective c

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.

Get all views of an app- every moment?

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.

Key-value observing doesn't work for zoomScale

I'm stuck with following problem. I have a UIScrollView, _myScrollView, and I want to have another UIScrollView following it's movements. So I'm using key-value observing for the properties "zoomScale" and "contentOffset", but the observeValueForKeyPath:ofObject:change:context: method only report changes in "contentOffset", not in "zoomScale", though the zooming workes fine. (See code snippet below.) What could be the reason for this?
-(void)viewDidLoad {
...
[_myScrollView addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];
[_myScrollView addObserver:self forKeyPath:#"zoomScale" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"zoomScale"]) {
NSLog(#"zoomScale: %#", change); // Never gets called
}
...
}
The zoomScale property isn't KVO compliant. But UIScrollViewDelegate has a scrollViewDidZoom method that you can use to track changes to zoomScale.
UIKit actually doesn't support KVO.
From the docs:
Note: Although the classes of the UIKit framework generally do not
support KVO you can still implement it in the custom objects of your application, including custom views.
It does work sometimes (as you've seen), but support for it is undocumented and inconsistent. Use the delegate methods instead.

How can I prepare a UISplitViewController secondary VC for a change to displayMode?

In iOS8, UISplitViewController changed, and now notifies its delegate of a pending displayMode change via splitViewController:willChangeToDisplayMode:. I need to update some aspects of my secondary view controller in response to this change.
It's simple enough to call methods on the secondary VC during this delegate method, but the VC doesn't yet know what its new bounds will be.
Is there a reasonable way to be notified that the bounds of the VC will change, apart from KVO on the bounds of the secondary VC? Ideally, the VC would have viewWillTransitionToSize:withTransitionCoordinator: called for the displayMode change, since that provides for the ability to animate alongside the transition.
So, for now I'm just using KVO. I followed some of the advice here.
In viewDidLoad:
[self.view addObserver:self
forKeyPath:NSStringFromSelector(#selector(frame))
options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew)
context:nil];
Then:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([object isKindOfClass:[UIScrollView class]] && [keyPath isEqualToString:NSStringFromSelector(#selector(frame))]) {
CGRect newFrame = [change[#"new"] CGRectValue];
CGRect oldFrame = [change[#"old"] CGRectValue];
if ((newFrame.size.width == oldFrame.size.width) || (newFrame.size.height == oldFrame.size.height)) {
// If one dimension remained constant, we assume this is a displayMode change instead of a rotation
// Make whatever changes are required here, with access to new and old frame sizes.
}
}
}
I tried this on the bounds of the view, but that fired a lot more frequently than the KVO on the frame.

What to use as the keypath in KVO?

I have a view controller with a view that changes (for example), and I would like to observe the frame of any view that self.view is set to.
Is there any difference between:
[self.view addObserver:self forKeyPath:#"frame" options:0 context:nil];
and
[self addObserver:self forKeyPath:#"view.frame" options:0 context:nil];
For the second one, if the view changes will messages still be recieved when the new view's frame changes, or will it only send messages if the frame of the view that was set when the observer was added?
Is there any way to observe changes to the frame property even if the view of the view controller changes after adding the observer?
Use the second path. #"view.frame" will notify you about the frame changes even when the "view" itself is changed. Cocoa will add observers for every object in the keyPath "chain" for you automatically (which means every item in the keyPath must be KVO-compatible).
You asked if there is a difference between the two, The answer is yes, there is a difference between them:
The first one
says "me as a view", I add an observer named self (aka) viewControllerObject, if you invoked this in viewController.m whenever my property named "frame" is changed.
The Second one
Says "me as ViewController" I'm adding myselfAsAnObserver whenever theKeyPath named "view.frame" is changed.
Since every observer should implement
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
For this case you won't notice much difference because you added a viewController as an observer in either of the method above, but it will make a difference when you are dealing with different objects. But the rule is simple, each added observer should implement the
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
One more thing:
Its a good idea to create a context for observation
e.g
//In MyViewController.m
//..
static int observingViewFrameContext
// In ...
[self addObserver:self
forKeyPath:#"view.frame"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:&observingViewFrameContext];
// .. don' forget to remove an observer ! too

Resources