I'm in a dilemma which method to use for setting frames of custom UIViews with many subviews in it and still have animations and automatically adjust to rotations. What I usually do when I create a new viewcontroller is alloc my custom view in loadView or viewDidLoad, e.g:
-(void)viewDidLoad
{
[super viewDidLoad];
detailView = [[DetailView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
detailView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
self.view = detailView;
}
Normally this width & height is not correct for an iPhone5-screen (the actual view-frame is not set until viewWillAppear) but because of the autoresizingmask it all works out.
Then in the initWithFrame of the custom UIView DetailView, I alloc all subviews with CGRectZero, e.g:
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
label = [[UILabel alloc] initWithFrame:CGRectZero];
[self addSubview:label];
}
}
Then I override layoutsubviews to set all frames of all subviews. This works perfectly for any screen size and any orientation, e.g:
-(void)layoutSubviews
{
[super layoutSubviews];
label.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
However, I just found out that layoutSubviews is not so great when you use animations, because when you use animations in an animationblock, layoutsubviews is called in the middle of the animation and it completely breaks the animation, e.g:
-(void)animateLabel
{
[UIView animateWithDuration:0.4f animations:^
{
label.transform = CGAffineTransformMakeTranslation(100, 100);
}];
}
I believe there are ugly workarounds this by using flags for each animation and in layoutsubviews use those flags to set the correct start or endframe of the animated block but I don't think I should have to create a flag for each animation I want to do.
So my problem is now: how am I supposed to have a custom UIView WITH animations that also automatically adjusts itself to rotations?
The only solution I can come up with right now (that I don't like):
Don't use layoutSubviews but use the setFrame/setBounds method of the custom UIView to set the frames of all subviews. Then check in the viewController every time a rotation occurs and then use the setFrame/setBounds method of the custom UIView to change all frames of all subviews. I don't like this solution because the rotation methods are different in iOS5 and iOS6 and I don't want to have to do this in every UIViewController with it's own custom UIView.
Any suggestions?
I have recently started overriding viewDidLayoutSubviews (many times instead of viewWillAppear) in my UIViewControllers.
Yes viewDidLayoutSubviews is called on rotations. (from comment)
The method fires after all the internal layouts have already been completed so all finalized frames should be setup, but still give you the time you need to make adjustments before the the view is visible and shouldn't have any issues with animations because you are not already inside an animation block.
viewcontroller.m
- (void)viewDidLayoutSubViews {
// At this point I know if an animation is appropriate or not.
if (self.shouldRunAnimation)
[self.fuView runPrettyAnimations];
}
fuView.m
- (void)runPrettyAnimations {
// My animation blocks for whatever layout I'd like.
}
- (void)layoutSubviews {
// My animations are not here, but non animated layout changes are.
// - Here we have no idea if our view is visible to the user or may appear/disappear
// partway through an animation.
// - This also might get called far more than we intend since it gets called on
// any frame updates.
}
Related
I have a uiview at the top of the interface (below the status bar) that only the bottom part of it is shown.
Actually, I want to make the red uiview to slide down to be entirely shown by drag such as the notificationcenter in the native iOS and not just by taping a button.
What should I use to "touch and pull down" the uiview so it could be shown entirely ?
No needs to find a workaround of drag-n-drop. An UIScrollView can do it without any performance loss brought by listening on touches.
#interface PulldownView : UIScrollView
#end
#implementation PulldownView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self) {
return self;
}
self.pagingEnabled = YES;
self.bounces = NO;
self.showsVerticalScrollIndicator = NO;
[self setBackgroundColor:[UIColor clearColor]];
double pixelsOutside = 20;// How many pixels left outside.
self.contentSize = CGSizeMake(320, frame.size.height * 2 - pixelsOutside);
// redArea is the draggable area in red.
UIView *redArea = [[UIView alloc] initWithFrame:frame];
redArea.backgroundColor = [UIColor redColor];
[self addSubview:redArea];
return self;
}
// What this method does is to make sure that the user can only drag the view from inside the area in red.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (point.y > height)
{
// Leaving useless touches to the views under it.
return nil;
}
return [super hitTest:point withEvent:event];
}
#end
How to use:
1. Initialize an instance of PulldownView.
2. Add any content you want to display to the instance using [addSubview:].
3. Hide the area in red.
[pulldownView setContentOffset:CGPointMake(0, heightOfTheView - pixelsOutside)];
This is a simple example. You can add any features to it like adding a titled button bar on the bottom of the draggable area to implement click-n-drop, or adding some method to the interface to reposition it by the caller.
Make a subclass of UIView.
Override touchesBegan:withEvent and touchesMoved:withEvent.
In the touchesBegan perhaps make a visual change so the user knows they are touching the view.
In the touchesMoved use
[[touches anyObject] locationInView:self]
and
[[touches anyObject] previousLocationInView:self]
to calculate the difference between the current touch position and the last touch position (detect drag down or drag back up).
Then if you're custom drawing, call [self setNeedsDisplay] to tell your view to redraw in it's drawRect:(CGRect)rect method.
Note: this assumes multiple touch is not used by this view.
Refer to my answer in iPhone App: implementation of Drag and drop images in UIView
You just need to use TouchesBegin and TouchesEnded methods. In that example, I have shown how to use CGPoint, Instead of that you have to try to use setFrame or drawRect for your view.
As soon as TouchesMoved method is called you have to use setFrame or drawRect (not sure but which ever works, mostly setFrame) also take the height from CGPoint.
I have a strange bug that I can't seem to figure out. I'm creating a little shining animation, which works perfectly, but for some reason stops when I navigate to another view via UINavigationController or UITabView (strangely modal view's don't affect it). Any ideas why, and how I can make sure the animation doesn't stop?
UIView *whiteView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[whiteView setBackgroundColor:[UIColor whiteColor]];
[whiteView setUserInteractionEnabled:NO];
[self.view addSubview:whiteView];
CALayer *maskLayer = [CALayer layer];
maskLayer.backgroundColor = [[UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:0.0f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"ShineMask.png"] CGImage];
// Center the mask image on twice the width of the text layer, so it starts to the left
// of the text layer and moves to its right when we translate it by width.
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(-whiteView.frame.size.width,
0.0f,
whiteView.frame.size.width * 2,
whiteView.frame.size.height);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:self.view.frame.size.width * 9];
maskAnim.repeatCount = HUGE_VALF;
maskAnim.duration = 3.0f;
[maskLayer addAnimation:maskAnim forKey:#"shineAnim"];
whiteView.layer.mask = maskLayer;
Your maskAnim is retained by maskLayer, which is retained by whiteView's layer, which is retained by whiteView, which is retained by self.view. So this entire object graph will live until your view controller's view gets dealloc'd.
When you navigate away from your view controller, UIKit unloads your view to free up memory. When the view gets dealloc'd, so does your maskAnim. When you navigate back to your view controller, UIKit reconstructs your view hierarchy, either by reloading it from its .xib, or by calling loadView, depending on which technique you used.
So you need to make sure that the code you used to set up your maskAnim gets called again after UIKit reconstructs your view hierarchy. There are four methods you might consider: loadView, viewDidLoad, viewWillAppear:, and viewDidAppear:.
loadView is a sensible option if you use that method to build your view hierarchy (as opposed to loading it from a .xib), but you'll have to change your code so that it doesn't depend on the self.view property, which would trigger loadView recursively. viewDidLoad is also a good choice. With either of these options you you need to be careful because UIKit might resize your view after viewDidLoad if it wasn't constructed at the right size. This could cause bugs since your code depends on self.view.frame.size.width.
If you set up your animation in viewWillAppear: or viewDidAppear:, you can be sure that your view's frame will be the proper dimension, but you need to be careful with these methods because they can get called more than once after the view gets loaded, and you don't want to add your whiteView subview more than once.
What I'd probably do is make whiteView a retained property, and lazy load it in viewWillAppear: like this:
- (UIView *)setupWhiteViewAnimation {
// Execute your code above to setup the whiteView without adding it
return whiteView;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (!self.whiteView) {
self.whiteView = [self setupWhiteViewAnimation];
[self.view addSubview:self.whiteView];
}
}
- (void)viewDidUnload {
self.whiteView = nil; // Releases your whiteView when the view is unloaded
[super viewDidUnload];
}
- (void)dealloc {
self.whiteView = nil; // Releases your whiteView when the controller is dealloc'd
[super dealloc];
}
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).
I have made a custom UIView which is shown when the user hits a button in the navigationbar. I make my view's in code. In my loadview I set the autoresizing masks and the view loads correct on screen. However the UIView which is shown when the user taps the button does not resize even when I have set the autoresizing masks.
UIView *blackView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 416.0)];
blackView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Do I need to use self.view.frame.size.width and self.view.frame.size.height instead? And if I do why? Does not resizing masks work outside of loadView?
Thank you for your time:)
the autoresizingMask affects how a view will behave when its superviews frame changes. if all you are doing is showing theblackViewwhen you tap a button, thenblackView` will have whatever frame you initially set for it.
If this isn't enough info, please post some more code around how you are configuring and displaying blackView and it's superview and explain more about what situations you are expecting blackView to resize in. Rotation is one of them, if that's what you're concerned with.
First things first, I hope you've done this:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
Let's say the view that needs resizing is: view2
The view that has view2 as a subview is: view1
While creating view1 you would declare it as:
view1 = [[UIView alloc] init];
[view1 setNeedsLayout];
Now in view1's .m file you need to overload the layoutSubviews method as shown:
- (void)layoutSubviews
{
CGRect frame = view2.frame;
// apply changes to frame
view2.frame = frame;
}
In case view1 is a view controller's view, you need to do that same thing as above in the willRotate method as shown
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
CGRect frame = view2.frame;
// apply changes to frame
view2.frame = frame;
}
This is a tried and tested method that I use to handle orientation changes.
By default, a UITableViewCell instance positions the label pair textLabel/detailTextLabel in the center of its parent view. I prefer to have the pair aligned to the top of the cell. How do I do that programmatically?
Thanks,
Doug
The docs describe -[UIView layoutSubviews] as
"Overridden by subclasses to layout subviews when layoutIfNeeded is invoked. The default implementation of this method does nothing."
That description is not elaborate, but is accurate. In this case, the method's behavior is to layout your subviews. It will be invoked anytime the device orientation changes. It is also scheduled for later invocation whenever you call -setNeedsLayout.
Since, UIView's implementation does nothing, (and I presume the same for UIControl), you get total creative freedom to make your UIView subclass subviews be positioned wherever you want them.
In subclassing a UITableViewCell, you have a couple of options to try:
Override -layoutSubviews and
manipulate the position of the built-in textLabel and -detailLabel views.
Override -viewDidLoad,
create two of your own UILabels to provide the text and detailed text,
add them to self.contentView, and
override -layoutSubviews to manipulate the position of your custom UILabel views
In a related SO question, the recommendation is to avoid #1, manipulating the built-in textLabel and detailTextLabel.
A more reliable bet would be to do something like this:
#interface MyTableViewCell : UITableViewCell {
UILabel *myTextLabel;
UILabel *myDetailTextLabel;
}
// ... property declarations
#end
#implementation MyTableViewCell
#synthesize myTextLabel, myDetailTextLabel;
- (id) initWithFrame: (CGRect)frame {
self = [super initWithFrame: frame];
if (self) {
myTextLabel = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease];
[self.contentView addSubview: myTextLabel];
myDetailTextLabel = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease];
[self.contentView addSubview: myDetailTextLabel];
}
return self;
}
- (void) dealloc {
[myTextLabel release];
[myDetailTextLabel release];
[super dealloc];
}
- (void) layoutSubviews {
// Let the super class UITableViewCell do whatever layout it wants.
// Just don't use the built-in textLabel or detailTextLabel properties
[super layoutSubviews];
// Now do the layout of your custom views
// Let the labels size themselves to accommodate their text
[myTextLabel sizeToFit];
[myDetailTextLabel sizeToFit];
// Position the labels at the top of the table cell
CGRect newFrame = myTextLabel.frame;
newFrame.origin.x = CGRectGetMinX (self.contentView.bounds);
newFrame.origin.y = CGRectGetMinY (self.contentView.bounds);
[myTextLabel setFrame: newFrame];
// Put the detail text label immediately to the right
// w/10 pixel gap between them
newFrame = myDetailTextLabel.frame;
newFrame.origin.x = CGRectGetMaxX (myTextLabel.frame) + 10.;
newFrame.origin.y = CGRectGetMinY (self.contentView.bounds);
[myDetailTextLabel setFrame: newFrame];
}
#end
In MyTableViewCell, you would ignore the built-in text labels and use your own custom UILabels. You take complete control over positioning them within the content rect of the table view cell.
I'm leaving a lot of stuff out. In doing custom layout with text labels, you'd want to consider:
Figuring out your own layout algorithm.
I'm using a layout algorithm above that resizes the custom UILabels to fit their text content, then positions them side-by-side. You'll likely want something more specific to your app.
Keeping the custom labels within the content view.
In -layoutSubviews, you might want logic to keep the custom UILabels sized and positioned so that they don't fall outside the bounds of the content rect. With my naive layout logic, any long text dropped into either UILabel (or both) could cause the labels to be positioned right out of the content view bounds.
How to handle -viewDidLoad/-viewDidUnload.
As coded above, this subclass doesn't handle being loaded from a nib. You might want to use IB to layout your cell, and if you do, you'll need to think about -viewDidLoad/-viewDidUnload/-initWithCoder:
The following method in your UITableViewCell subclass should quickly and concisely align both textLabel and detailTextLabel to the top of the cell (nod to Bill's code), without adding any custom views.
- (void) layoutSubviews
{
[super layoutSubviews];
// Set top of textLabel to top of cell
CGRect newFrame = self.textLabel.frame;
newFrame.origin.y = CGRectGetMinY (self.contentView.bounds);
[self.textLabel setFrame:newFrame];
// Set top of detailTextLabel to bottom of textLabel
newFrame = self.detailTextLabel.frame;
newFrame.origin.y = CGRectGetMaxY (self.textLabel.frame);
[self.detailTextLabel setFrame:newFrame];
}
Layout of the UITableViewCell textLabel and detailTextLabel are not directly modifiable, except by picking one of the defined styles provided by the API.
typedef enum {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
} UITableViewCellStyle;
If you want to customize UITableViewCell layout, you'll need to subclass it and override the -layoutSubviews method.
- (void) layoutSubviews {
// [super layoutSubViews]; // don't invoke super
... do your own layout logic here
}