How to detect when a property of a class is changed - iphone

When changing the frame of a UIView, you have two options. The first would be to pass a CGRect as a parameter to the setFrame function of a view. The other would be to set view.frame equal to the CGRect.
1) [view setFrame:frame];
2) view.frame = frame;
When using the setFrame function of a UIView, it is easy for the view to detect that it should change its frame. However, when simply changing the property (view.frame), how does the view detect that its frame changed (because the view's frame updates immediately)?

That's called dot notation which came with Objective-C 2.0. They're mostly use to get and set properties.
Get a property: view.frame. Set a property: view.frame = frame.
But they're just another way or easier way (syntactic sugar) to write: [view frame] and [view setFrame:frame].
That's all they are, they're just translated to plain old Objective-C messages. In fact, since they're just translated to messages, try something like:
view.setNeedsDisplay;
Note that setNeedsDisplay is not actual property, but since it's translated to [view setNeedsDisplay], it will work! By convention, dot notation is only use for getting and setting properties, so I don't suggest using something like view.setNeedsDisplay.

view.frame = frame equals [view setFrame:frame]. Setting a property using dot method is same as using setXXX:XXX. You can put a NSLog in setXXX:, and call obj.XXX = XXX; And check the console to see if the NSLog is printed.
Example:-
view.frame = frame; //If you set frame like this then
-(void)setFrame:(CGRect)frame
{
//this will be call
}

Related

User interaction outside the UIView bounds

What i'm trying to do is "navigating" into a bigger UIView with buttons and controls positioned in different places. I've made the size of the main UIView twice bigger than the usual. It is 640x480 instead of 320x480.
After clicking a button in the first part of the screen i've made a moving translation of -320px in the x-direction to show the second "hidden" part of the screen where other functions will reveals to the user. Everything works perfect apart the fact i can't get the UIView back to the original position. It seems that the button i use to get back to the original position and that is "outside the bounds" of the 320px, doesn't work.
My UIView is referenced as "introScreen", and the following is the function i call to translate the screen through the x direction:
- (void)secondPartOfTheScreen {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.15];
introScreen.transform = CGAffineTransformMakeTranslation(-320, 0);
[UIView commitAnimations];
}
Calling this function my UIView moves correctly, but when my button appear in this part of the screen, it doesn't get any user interaction. If i move that button in the first part of the screen, it works correctly. It have something to do with the screen bounds? It is possibile to solve it?
Thanks in advance!
You should try to subclass your UIView and override its pointInside:withEvent: method so that also a point outside its bounds is recognized as belonging to the view. Otherwise, there is no chance that user interaction outside of the view bounds are handled as you would like to.
This is what the Event Handling Guide say:
In hit-testing, a window calls hitTest:withEvent: on the top-most view of the view hierarchy; this method proceeds by recursively calling pointInside:withEvent: on each view in the view hierarchy that returns YES, proceeding down the hierarchy until it finds the subview within whose bounds the touch took place. That view becomes the hit-test view.
You could use something like this in your custom UIView:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView* view in self.subviews) {
if (view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}
You could do with this:
#interface MyView: UIView
#end
#implementation MyView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView* view in self.subviews) {
if (view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}
#end
add this class definition to the beginning of the .m file where you are using the view. (You could also use separate .h/.m files, but for the sake of testing if everything works, it is enough). Then, replace UIView with MyView in the instantiation of your current UIView, and it should work.
You're not using the transform property properly, you should take into account the previous value of the property like this:
introScreen.transform = CGAffineTransformConcat(introScreen.transform, CGAffineTransformMakeTranslation(-320, 0));
Hope this helps, and check Apple's UIView documentation and CGAffineTransform documentation for more details.
2 things.
First why would you use transform when you are merely moving the object. You can set the frame just as easily.
CGRect frame = introScreen.frame;
frame.origin.x -= 320;
introScreen.frame = frame;
(Yes this will work inside an animation)
second, if you translate a view its contents should be within its bounds regardless of the size you want displayed in the first place.
therefore your container view (the one with the buttons) should be a size so that all contained pieces are within its bounds. Anything outside its bounds will not function. Having the main view twice its size is not as useful as having the contained view at twice its size.
Think of it like this.
Your table should be able to hold whatever your looking at. If you have a map you want to display you would slide it around on the table. the table does not need to be as big as the map but the map needs to be big enough to present you with all of its contents. if the map was say too short to display the poles and you would be without the information. No matter how much you transformed the map you would not be able to use the north or south poles..
To ensure the app reflects this you can turn on clip subviews on all the views in question, then if its outside the bounds it will not be visible.

On iOS, why changing the frame of a CALayer didn't cause an animation?

I have some iOS sample code that if I tap on the main view and the tap handler changes the CALayer object's frame property, the layer object will animate to its position and size.
But if I put that animation inside ViewController's viewDidLoad:
UIImage *shipImage = [UIImage imageNamed:#"SpaceShip1.png"];
ship1 = [[CALayer alloc] init];
ship1.contents = (id) [shipImage CGImage];
ship1.frame = CGRectMake(50, 50, 100, 100);
[self.view.layer addSublayer:ship1];
ship1.frame = CGRectMake(0, 0, 300, 200);
then the animation won't happen. It will just use the (0, 0, 300, 200) with no animation. Why is that and how to make it work?
Several things.
First of all, the frame property is not animatable. Instead, you should be animating bounds (to change the size) and position (to move the layer).
To quote the docs:
Note: The frame property is not directly animatable. Instead you
should animate the appropriate combination of the bounds, anchorPoint
and position properties to achieve the desired result.
Second, you have to return to the event loop after adding a layer before changing an animatable property.
When you return and the system visits the event loop, the next time you change an animatable property, it creates an animation transaction to animate that change.
Since you created a layer, set it's frame, and then changed it, all before returning, your code simply adds the layer at it's final frame.
What you should do instead is to set up the layer, install it into it's parent layer, and return. You can use performSelector:withObject:afterDelay: to invoke the animation after the layer has been added:
- (void)viewDidLoad
{
UIImage *shipImage = [UIImage imageNamed:#"SpaceShip1.png"];
ship1 = [[CALayer alloc] init];
ship1.contents = (id) [shipImage CGImage];
ship1.frame = CGRectMake(50, 50, 100, 100);
[self.view.layer addSublayer:ship1];
[self performSelector: #selector(changeLayer)
withObject: nil
afterDelay: 0.0];
}
- (void) changeLayer
{
//Animate changes to bounds and position, not frame.
ship1.bounds = CGRectMake(0, 0, 300, 200);
//Note that position is based on the center of the layer by default, so you'll
//need to adjust your desired position.
ship1.position = CGPointMake(0,0);
}
You have specified no time delay or animation time. So the object will be moved before it is displayed, and you won't see it in the earlier position. You also need to return to the run loop for the display to show any animation.
There are 3 ways to trigger animations on iOS:
1. UIView Animation
You use a UIView.animateWithDuration method or other variant to directly specify that you want an animation. You can specify parameters such as duration, delay, a completion block and others.
2. Core Animation - Implicit Animation (DESIRED APPROACH)
As the name implies, this animation is triggered without an explicit command. In order to work though, you need to set the CALayer delegate, like this:
ship1.delegate = self;
All the parameters have default values until you say otherwise. For modifying these parameters, you need to call CATransaction:
[CATransition setAnimationDuration:0.2];
IMPORTANT: These CATransaction calls must be placed BEFORE the modifications of the layer's properties, because all animations happen asynchronous (in different runtime threads) and it will be too late for you if you put these calls after.
3. Core Animation - Explicit Animation
You create an CAAnimation (there are different CAAnimation subclass to choose from) and attach it to the layer:
CABasicAnimation *a = [CABasicAnimation alloc] init....
[ship1 addAnimation:a];
I hope this clarifies some doubts about the subject.

Animate a CALayer's bounds w/redraws

I am wondering how one might animate a CALayer's bounds so, on each bounds change, the layer calls drawInContext:. I've tried the 2 following methods on my CALayer subclass:
Setting needsDisplayOnBoundsChange to YES
Returning YES for the + (BOOL)needsDisplayForKey:(NSString*)key for the bounds key
Neither work. CALayer seems determined to use the layer's original contents and simply scale them according to contentsGravity (which, I assume, is for performance.) Is their a workaround for this or am I missing something obvious?
EDIT: And, incidentally, I noticed that my custom CALayer subclass is not calling initWithLayer: to create a presentationLayer - weird.
Thanks in advance,
Sam
You can use the technique outlined here: override CALayer's +needsDisplayForKey: method and it will redraw its content at every step of the animation.
I'm not sure that setting the viewFlags would be effective. The second solution definitely won't work:
The default implementation returns NO. Subclasses should * call super
for properties defined by the superclass. (For example, * do not try
to return YES for properties implemented by CALayer, * doing will
have undefined results.)
You need to set the view's content mode to UIViewContentModeRedraw:
UIViewContentModeRedraw, //redraw on bounds change (calls -setNeedsDisplay)
Check out Apple's documentation on providing content with CALayer's. They recommend using the CALayer's delegate property instead of subclass, which might be a lot easier than what you're trying now.
I don't know if this entirely qualifies as a solution to your question, but was able to get this to work.
I first pre-drew my contents image into a CGImageRef.
I then overrode the -display method of my layer INSTEAD OF -drawInContext:. In it I set contents to the pre-rendered CGImage, and it worked.
Finally, your layer also needs to change the default contentsGravity to something like #"left" to avoid the contents image being drawn scaled.
The problem I was having was that the context getting passed to -drawInContext: was of the starting size of the layer, not the final post-animation size. (You can check this with the CGBitmapContextGetWidth and CGBitmapContextGetHeight methods.)
My methods are still only called once for the entire animation, but setting the layer's contents directly with the -display method allows you to pass an image larger than the visible bounds. The drawInContext: method does not allow this, as you cannot draw outside the bounds of the CGContext context.
For more about the difference between the different layer drawing methods, see http://www.apeth.com/iOSBook/ch16.html
I'm running into the same problem recently. This is what I found out:
There is a trick you can do it, that is animating a shadow copy of bounds like:
var shadowBounds: CGRect {
get { return bounds }
set { bounds = newValue}
}
then override CALayer's +needsDisplayForKey:.
However, this MAY NOT be what you want to do if your drawing depends on bounds. As you have already noticed, core animation simply scales the contents of layer to animate bounds. This is true even if you do the above trick, that is, the contents are scaled even if the bounds changed during animation. The result is your animation of drawings looks inconsistent.
How to resolve it? Since the content is scaled, you can calculate the values of custom variables determining your drawing by reverse-scaling them so that your drawing on the final but scaled content looks the same as the original and unscaled one, then set the fromValues to these values, the toValues to their old values, animate them at the same time with bounds. If the final values are to be changed, set the toValues to these final values. You must animate at least one custom variable so as to causing the redraw.
This is your custom class:
#implementation MyLayer
-(id)init
{
self = [super init];
if (self != nil)
self.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], #"bounds",
nil];
return self;
}
-(void)drawInContext:(CGContextRef)context
{
CGContextSetRGBFillColor(context,
drand48(),
drand48(),
drand48(),
1);
CGContextFillRect(context,
CGContextGetClipBoundingBox(context));
}
+(BOOL)needsDisplayForKey:(NSString*)key
{
if ([key isEqualToString:#"bounds"])
return YES;
return [super needsDisplayForKey:key];
}
#end
These are additions to xcode 4.2 default template:
-(BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
// create and add layer
MyLayer *layer = [MyLayer layer];
[self.window.layer addSublayer:layer];
[self performSelector:#selector(changeBounds:)
withObject:layer];
return YES;
}
-(void)changeBounds:(MyLayer*)layer
{
// change bounds
layer.bounds = CGRectMake(0, 0,
drand48() * CGRectGetWidth(self.window.bounds),
drand48() * CGRectGetHeight(self.window.bounds));
// call "when idle"
[self performSelector:#selector(changeBounds:)
withObject:layer
afterDelay:0];
}
----------------- edited:
Ok... this is not what you asked for :) Sorry :|
----------------- edited(2):
And why would you need something like that? (void)display may be used, but documentation says it is there for setting self.contents...

Override UIView's frame

I have subclassed UIView, but I need to prevent the frame from changing, so I tried overriding the setFrame: method and just ignoring the value passed, and creating my own CGRect base on self.superview and passing it to [super setFrame:
How can I make the UIView's frame unchangeable? (from within the subclass)
Step1: Override setFrame to do nothing.
- (void)setFrame:(CGRect)newRect
{
}
Step 2: Move your semi-static frame setting into your overridden version of willMoveToSuperview: to get a valid superview reference.
- (void)willMoveToSuperview:(UIView *)newSuperview
{
CGRect newFrame = newSuperview.frame;
//manipulate the frame here
[super setFrame:newFrame];
}
Wait, can't you just set the autoresizingMask to 0? I though that would prevent the view from getting resized when parent view is resized (tried it as well, and seemed to work for me).

iphone, objective c, xcode

I defined a UIView "RowOfThree" inwhich there are 3 labels. i also defined a UIView "Table" inwhich there are numer of objects of type "Row".
the following code is in a method within object "Table":
RowOfThree *rowOfThree = [[RowOfThree alloc] init];
[self addSubview:rowOfThree];
for some reason it doesn't add the view.
i tried defining the labels in "RowOfThree" both in IB and programmatically and it still didn't work.
Thank you.
Typically, a UIView (and the subclasses) are initialized using initWithFrame:. Maybe you did that in your own implementation of init, I don't know, but it may very well be that your view has a frame of {0,0,0,0} and therefore 0 height and 0 width. Set the frame by hand and tell us whether this works.
CGRect newFrame = CGRectMake(0.f, 0.f, 200.f, 40.f);
RowOfThree *rowOfThree = [[RowOfThree alloc] initWithFrame:newFrame];
[self addSubview:rowOfThree];
[rowOfThree release];
SanHalo's answer is most likely correct but in addition, if you're using Interface Builder, you should not be directly initializing views that are defined in the nib. If you do, you have to use initFromNib instead of just init.