I have a UIViewController with many subviews like UILabels, UIImages and a UIWebview. With a defined action by the user, the subviews of the UIViewController animate to different sizes and different locations inside of the UIViewController's view. Is it possible that this can be undone with a different defined action by the user? I want to make all the subviews revert back to their previous locations and sizes that they were before the animation was run. I thought of two possible solutions:
Get the properties of the subviews with the view.subviews() method before the animation is run, and then set the subviews after the animation to the properties in this array, or,
Call a method on the UIViewController to tell it to redraw all the subviews according to the properties set in the storyboard file.
Are these the right way of accomplishing what I would like to do? And if so, how would I go about doing this? (I don't know how to programmatically implement either of my ideas.)
Any help is greatly appreciated. Thanks.
Here is the solution.
#interface ViewController ()
#property (strong, nonatomic) NSMutableArray *frames;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//Saving initial frames of all subviews
self.frames = [NSMutableArray new];
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
CGRect frame = view.frame;
NSValue *frameValue = [NSValue valueWithCGRect:frame];
[self.frames addObject:frameValue];
}
}
- (NSMutableArray *)allViewsOfView:(UIView *)view
{
NSMutableArray *result = [NSMutableArray new];
[result addObject:view];
for (UIView *subView in view.subviews) {
[result addObjectsFromArray:[self allViewsOfView:subView]];
}
return result;
}
- (void)resetFrames
{
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
NSValue *frameValue = [self.frames objectAtIndex:[allViews indexOfObject:view]];
CGRect frame = [frameValue CGRectValue];
view.frame = frame;
}
}
#end
Call [self resetFrame]; whenever you want to revert view's frames back to their initial values.
You could cache all your subview's frame before changing it and running the animation, in this way you can even cache more than one action. A stack structure will be perfect for this, but there is no way to achieve this in interface builder, you have to reference outlets from IB to code to get their frame.
Related
I have created a method
- (UIView *) drawView:(NSMutableArray *) arrayToDisplay;
wich works perfectly if it is called by another method, e.G.
-(void) preperationsBeforeDrawView.
But inside of arrayToDisplay I have a loop which gets activated if it finds a special object.
- (UIView *) drawView:(NSMutableArray *) arrayToDisplay {
[UIView * overAllView = [[UIView alloc] initWithFrame:CGRectZero];
while ([arrayToDisplay containsObject:myObject]) {
NSMutableArray *subArray = [[arrayToDisplay subarrayWithRange:NSRange] mutableCopy];
UIView *result = [self drawView:subArray];
[overAllView addSubView:result];
}
/*
* All the others drawView things
* Create Labels & Views
* add them as subview
*/
[overAllView sizeToFit];
return overAllView;
}
So, if the while-Loop is calling [self drawView:subArray] the returned UIView doesn't sizeToFit, even if it has subViews which have (width, hight != 0). It doesn't show any effect no matter if I call sizeToFit inside drawView or after. But the returned UIView from the first called drawView works perfectly :/.
Can you please tell me where I made the mistake?
I have a messaging screen I am creating and I am almost done it. I built most of the views with nib files and constraints. I have one small bug however where I can visually see some of the cells laying themselves out when the keyboard dismisses because of the requirement to call [self.view layoutIfNeeded] in an animation block involving a constraint. Here is the problem:
- (void)keyboardWillHide:(NSNotification *)notification
{
NSNumber *duration = [[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey];
NSNumber *curve = [[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
[UIView animateWithDuration:duration.doubleValue delay:0 options:curve.integerValue animations:^{
_chatInputViewBottomSpaceConstraint.constant = 0;
// adding this line causes the bug but is required for the animation.
[self.view layoutIfNeeded];
} completion:0];
}
Is there any way around directly calling layout if needed on the view since this also causes my collection view to lay itself out which makes the cells layout visually on screen sometimes.
I tried everything I can think of but it I can't find a solution to the bug fix. I have already tried calling [cell setNeedLayout]; in every location possible, nothing happens.
How about this?
In your UITableViewCell implement a custom protocol called MYTableViewCellLayoutDelegate
#protocol MYTableViewCellLayoutDelegate <NSObject>
#required
- (BOOL)shouldLayoutTableViewCell:(MYTableViewCell *)cell;
#end
Create a delegate for this protocol
#property (nonatomic, weak) id layoutDelegate;
Then override layoutSubviews on your UITableViewCell:
- (void)layoutSubviews {
if([self.layoutDelegate shouldLayoutTableViewCell:self]) {
[super layoutSubviews];
}
}
Now, in your UIViewController you can implement the shouldLayoutTableViewCell: callback to control whether the UITableViewCell gets laid out or not.
-(void)shouldLayoutTableViewCell:(UITableViewCell *)cell {
return self.shouldLayoutCells;
}
Modify your keyboardWillHide method to disable cell layout, call layoutIfNeeded, and restore the cell layout ability in the completion block.
- (void)keyboardWillHide:(NSNotification *)notification {
NSNumber *duration = [[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey];
NSNumber *curve = [[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
self.shouldLayoutCells = NO;
[UIView animateWithDuration:duration.doubleValue delay:0 options:curve.integerValue animations:^{
_chatInputViewBottomSpaceConstraint.constant = 0;
[self.view layoutIfNeeded];
} completion:completion:^(BOOL finished) {
self.shouldLayoutCells = NO;
}];
}
I can't really test this since you didn't provide sample code, but hopefully this will put you on the right path.
I have a screen, which contains multiple UIImages (their amount and size are known only at runtime, so i add them programmatically) and some fixed buttons below these UIImages.
How to make buttons display certainly under all Images?
I've tried
1.) Put Buttons and Images into 2 separate views, and then add constraint between them. No result, buttons are hidden behind images.
2.) Put buttons into separate view and set constraint in code, (tried both viewDidLoad and viewDidAppear). Constraint is set between container view and top of the screen, depending on size and amount of images.
Example of code:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSInteger totalImages = [self.object.fullphotos count];
self.labelsTopConstraint.constant = totalImages*(imageHeight + 20) + 10;
}
In case 2 buttons are positioned right, but don't respond to touches.
How should I layout everything correctly?
Thanks in advance!
Take a Tableview for those images and add buttons in a last cell.
The best way is creating a Object with a refresh method that can be called in viewDidAppear
MyObject.h
#interface MyObject : UIViewController
#property (nonatomic,strong) UIImageview *img;
#property (nonatomic,strong) UIButton *btn;
- (void) refresh;
in MyObject.m
- (void)viewDidLoad {
[self.btn addTarget:self action:#selector(myMethod:) forControlEvents:UIControlEventTouchUpInside];
}
- (void) refresh {
//make your settings here
}
-(void)myMethod {
//your button action here
}
Then in your controller if you have your objects in an NSArray:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
for (MyObject *myObj in objectsArray) {
#autoreleasePool {
[myObj refresh];
}
}
}
In my app I habe a view controller that calls several views. All these views are UIViews. That works fine, but not in every case. One of the views that are called has some labels, textfields and two UITextViews. Everything is shown correctly but the UITextViews. The view is called in that way:
[[self view] addSubview:tasteView];
//tasteView = [[TasteView alloc] init];
[self setCurrentView:tasteView];
I call the init method of the view to display the UITextViews:
EDIT: After a comment of Phillip Mills this was slightly changed! Init isn't called anymore.
- (id)init
{
if (self)
{
[tv1 setNeedsDisplay];
CGRect frame = tv1.frame;
frame.size.height += 1;
tv1.frame = frame;
}
return self;
}
As I saw that setNeedsDisplay had no effect, I changed the size of the corresponsing frame to force a redraw. Unfortunately that had no effect, too.
Btw, the view is initially loaded in the viewDidLoad of the view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
[self setCurrentView:placeholder];
[self configureView];
wineryView = [self loadWineryView];
wineView = [self loadWineView];
tasteView = [self loadTasteView];
}
A method for loading the views looks like this:
- (UIView *) loadTasteView
{
NSArray *nibViews = [[NSBundle mainBundle] loadNibNamed:#"TasteView" owner:self options:nil];
UIView *tView;
for (id view in nibViews)
{
if ([view isKindOfClass:[TasteView class]])
{
tView = (TasteView*) view;
}
}
return tView;
}
I do not know why those UITextViews are not shown. Did I forget something? To show really everything, here are the connections that I made in InterfaceBuilder:
Does anyone know what I did wrong and can help me?
I think your initial code should be like this :
tasteView = [[TasteView alloc] init];
[[self view] addSubview:tasteView];
[self setCurrentView:tasteView];
addSubView after it is allocated
Hope it helps you
If you are creating the view in code (your first sample), alloc and init the view before trying to add it as a subview.
If you're loading it from another nib (last code section), you still need to add it to the view hierarchy.
Ok, here's another question.
I am creating a UIView called ProgressView that is a semi-transparent view with an activity indicator and a progress bar.
I want to be able to use this view throughout different view controllers in my app, when required.
I know of 3 different ways of doing this (but I am only interested in one):
1) Create the entire view programatically, instantiate and configure as required. No worries I get that one.
2) Create the UIView in interface builder, add the required objects and load it using a method like the below. Problem with this is that we are basically guessing that the view is the objectAtIndex:0 because nowhere in the documentation I found a reference to the order of the elements returned from the [[NSBundle mainBundle] loadNibName: function.
NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:#"yournib"
owner:self
options:nil];
UIView *myView = [nibContents objectAtIndex:0];
myView.frame = CGRectMake(0,0,300,400); //or whatever coordinates you need
[scrollview addSubview:myView];
3) Subclass UIViewController and let it manage the view as per normal. In this case I would never be actually pushing the view controller onto the stack, but only its main view:
ProgressViewController *vc = [[ProgressViewController alloc] initWithNibName:#"ProgressView" bundle:nil];
[vc.view setCenter:CGPointMake(self.view.center.x, self.view.center.y)];
[self.view addSubview:vc.view];
[vc release];
As far as I can tell, #3 is the the correct way of doing this (apart from programatically) but I am not entirely sure if it is safe to release the ProgressView's view controller whilst another controller's view is retaining its main view (gut feel says it is going to leak?)?
What do I do in terms of memory management in this case, where and when should I release the ProgressView's view controller?
Thanks in advance for your thoughts.
Cheers,
Rog
I think that your solution #3 adds unnecessary complexity by introducing a UIViewController instance just as a container for your ProgressView so that you can setup nib bindings. While I do think that it is nice to be able to work with an IBOutlet bound property rather than iterating through the nib contents you can do so without introducing a UIViewController whose behavior you neither need nor want. This should avoid your confusion around how and when to release the view controller and what, if any, side effects it might have on the responder chain or other behaviors of the loaded view.
Instead please reconsider using NSBundle and taking advantage of the power of that owner argument.
#interface ProgressViewContainer : NSObject {
}
#property (nonatomic, retain) IBOutlet ProgressView *progressView;
#end
#implementation ProgressViewContainer
#synthesize progressView = progressView;
- (void) dealloc {
[progressView release];
[super dealloc];
}
#end
#interface ProgressView : UIView {
}
+ (ProgressView *) newProgressView;
#end
#implementation ProgressView
+ (ProgressView *) newProgressView {
ProgressViewContainer *container = [[ProgressViewContainer alloc] init];
[[NSBundle mainBundle] loadNibNamed:#"ProgressView" owner:container options:nil];
ProgressView *progressView = [container.progressView retain];
[container release];
return progressView;
}
#end
Create a nib named "ProgressView" containing a ProgressView and set it's File's Owner class to ProgressViewContainer. Now you can create ProgressViews loaded from your nib.
ProgressView *progressView = [ProgressView newProgressView];
[scrollView addSubview:progressView];
[progressView release];
If you have multiple configurations of your progress view then maybe you'll want to implement a -initWithNibNamed: method on ProgressView instead of +newProgressView so you can specify which nib to use to create each ProgressView instance.
I vote for option #2. The return value from -[NSBundle loadNibNamed] is an array of the top-level objects. So as long as you have just one top level object in your nib, then the index 0 will be correct. The other views are subviews and not top level objects.
Another option of course is to do something like create a superclass for all of your view controllers that includes an outlet called something like 'progressView' and then connect your view to that outlet on file's owner in the nib. Seems like overkill for this, though.
I also prefer alternative #2. If the "0" is bothering you, you could:
Create a subclass of UIView called ProgressView
Create a nib-file called ProgressView.xib describing your progress view.
Select the topmost view in your nib, and set its Class to ProgressView in interface builder
then do
NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:#"ProgressView" owner:self options:nil];
ProgressView *progressView = nil;
for (UIView *view in nibContents) {
if ([view isKindOfClass:[ProgressView class]]) {
progressView = (ProgressView *) view;
break;
}
}
if (progressView != nil) {
//Use progressView here
}
I ended up adding a category to UIView for this:
#import "UIViewNibLoading.h"
#implementation UIView (UIViewNibLoading)
+ (id) loadNibNamed:(NSString *) nibName {
return [UIView loadNibNamed:nibName fromBundle:[NSBundle mainBundle] retainingObjectWithTag:1];
}
+ (id) loadNibNamed:(NSString *) nibName fromBundle:(NSBundle *) bundle retainingObjectWithTag:(NSUInteger) tag {
NSArray * nib = [bundle loadNibNamed:nibName owner:nil options:nil];
if(!nib) return nil;
UIView * target = nil;
for(UIView * view in nib) {
if(view.tag == tag) {
target = [view retain];
break;
}
}
if(target && [target respondsToSelector:#selector(viewDidLoad)]) {
[target performSelector:#selector(viewDidLoad)];
}
return [target autorelease];
}
#end
explanation here: http://gngrwzrd.com/blog-view-controller-less-view-loading-ios-mac.html