UIViews revert to a previous position after adding a subview - ios

Facing a really strange issue trying to dynamically add a left navigation panel to a View Controller, (should be able to support any view controller in the app, vaguely similar to the Facebook navigation) My idea seemed fairly simple, but I'm really not seeing where it's breaking down. What I've done is created a Category on UIViewController with the following method which I would think would move all the subviews to the right, and then add the new view.
-(void)addLeftView:(UIView *)newView
{
newView.frame=CGRectMake(0, 0, newView.frame.size.width, self.view.frame.size.height);
for(UIView *view in [self.view subviews])
{
view.frame=CGRectMake(view.frame.origin.x + newView.frame.size.width, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
}
[self.view addSubview:newView];
}
What actually happens, though, is that the view is added, but the subviews do not move to the right. However, if you comment the addSubview out, everything actually does move to the right exactly as expected.
To make matters even weirder, if you wrap the view movement in a [UIView animateWithDuration:completionHandler:], where the completion handler adds the subview, the animation actually happens - all the views shift to the right, but when the subview gets added, they jump back to their starting position.
I assumed this was some sort of wacky auto-layout issue, so just to see what happened, I cleared all the constraints out of that view controller, and get the same result.

Found the answer in a similar but un-related thread. Can I disable autolayout for a specific subview at runtime?
Basically it was auto-layout reverting my positioning, so disabling it by setting this user variable on just the UIViews of problem fixed my issues.
You can set the translatesAutoresizingMaskIntoConstraints type Boolean, Value to Yes in the User Defined Runtime Attributes of the UIView you want in the xib/storyboard.

Related

UIScrollView Frame Always CGRectZero

I have a nib that has the standard UIView and I've also included a UIScrollView as an IBOutlet (yes, it is hooked up), not as a subview of the main UIView, but just out on its own. Autolayout is turned off. The scroll view has several subviews and is larger than the main view. In viewWillAppear:, I set the content size of the UIScrollView to its current frame size, and then set its frame to the size of the main view, and add it as a subview of the main view.
Unfortunately, nothing is showing up. When I NSLog the frame of my UIScrollView, it is coming back as {0, 0, 0, 0} (CGRectZero). I thought this was odd, so I went back and tried logging the frame before I do any changes to it. Still zeroes. Logged it out in viewDidLoad before anything is done to any of my view elements. Still zeroes. (FWIW in my nib, the frame is {0, 0, 320, 896})
I've had this issue with several of my controllers, but it seems to be hit or miss. Sometimes it works, other times I get the empty frame. Typically, recreating everything from scratch seems to fix the issue, but I don't know why, as I'm setting everything up the same both times.
Running Xcode version 6.1 (6A1052d), iOS SDK 8.1 with a deployment target of 7.0
Let me know if there is any other relevant information I can give that might help.
EDIT 1: To address the questions about my UIScrollView being a "subview", here is what my view heirarchy looks like in the document outline:
As you can see, the UIScrollView is a "subview" of the view controller, but is not a subview of the "main" UIView which has the controller's view outlet.
EDIT 2: More images and some code. Here is a better look at how my nib is set up:
I add my scrollView to the main UIView as follows:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
scrollView.contentSize = scrollView.frame.size;
scrollView setFrame:CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.height - 100); // I slightly modified this because there are other variables that determine its Y position and height
[self.view addSubview:scrollView]
}
But I still don't think any of that code matters- for whatever reason, my UIScrollView is coming back nil. If I have the view created in my nib, the outlet is connected properly, how would the view still be nil? I'm creating my controller via initWithNibName I've tried cleaning the project, and removing the app and reinstalling.
Best guess on an answer, and some comments:
Nibs are simply serialised object trees. You can have as many view as you want as root. The problem (big one!) is that if they are not "connected" to outlets that retain them, at some point you will loose those references.
Since your UIScrollView is not a subview of the main view controller view on the Nib, it's not retained by it (views retain their subviews). So, it's up to you to retain it on your view controller.
My guess is that at the point you are trying to set the frame in the View Controller, the scroll view is already gone. I'm not sure how sure when are you doing it, but it might be after some run loops, so the unretained scroll view is dealloced.
Workarounds:
Instead of a variable for the IBOutlet, use a strong property (#property (strong, nonatomic) IBOutlet UIScrollView *scrollView).
Add the __strong qualifier to the variable.
Add the UIScrollView as a subview within the nib, so when the object tree is deserialised it's already retained by its parent.
Given that you purposefully put the UIScrollView outside of the main view hierarchy, I'm assuming you don't want option 3, so I'll just go with 1.
First, you might want to consider using autolayout. It makes things a lot easier once you make the investment to understand how it works. Check out this Apple doc on using UIScrollview with autolayout.
If that's not an option, the problem is that you're setting up your scrollView at the wrong point in the cycle. The scrollView needs a chance to lay itself out. By the time you hit viewWillAppear, you've missed sub view layout.
So, you could try:
Configure your scrollView in willLayoutSubviews and set its setNeedsLayout property.
Explicitly call layoutIfNeeded on your scrollView in viewWillAppear. The idea is to force another subview layout cycle. But, I'm not 100% sure it would work -- it might be too late at that point.
Note that the size of the main view (self.view) isn't determined until viewDidAppear. It's not always correct in viewWillAppear. So, you have a problem in your setup: you can't really set up your scrollView until you know your view size, but you don't know that until it's too late! You might want to redesign to avoid that dependency.

Add a subview from within and below another subview

In an iOS app, I am adding a subview to my main view with:
[self.view addSubview:firstUIImageSubview];
However in the subview class firstUIImageSubview, I am creating and adding another subView (secondUIImageSubview) that I would like to put below the first subview with:
[self insertSubview:secondUIImageSubview belowSubview:self];
My problem is that the second subview is displayed above the first subview when I want to have it below. How is it possible to achieve that ? Thanks.
This should do the trick.
[self.superview insertSubview:secondUIImageSubview atIndex:0];
When you use insertSubview:belowSubview: it places the subview in regards to other subviews that particular object manages.
[self insertSubview:secondUIImageSubview belowSubview:self];
Doesn't make much sense. Although self is a UIView (or a subclass) it still should never manage itself as a subview. Therefore
[self insertSubview:secondUIImageSubview belowSubview:firstUIImageSubview];
is probably what you want. But remember this will only place the secondUIImageSubview below firstUIImageSubview in terms of its Z-Index (it's depth on the screen). If you want it to be physically placed below firstUIImageSubview (IE it's XY coordinate) then you need to set it's position using subview's frame or setting its origin instead (by manipulating it's center or anchor points for instance).

UIView animation does not animate subviews

I have a UIScrollView with some UIViews, which again contain all kinds of other views (labels, text fields, etc.). Now, when I rotate the device I would like to alter the size and position of the views within the scroll view. In order to do so I calculate a set of constraints for both portrait and landscape mode up front and switch the constraints on the scroll view according to the current interface orientation. Basically, this works as expected. The thing is that I do not want the views to be simply put, but smoothly animated into their new positions. And this is where I am completely stuck right now. The basic animation works just fine, i.e. the views within the scroll view are perfectly animated but the subviews within these views are simply repositioned and not animated. I use the following code to kick off the animation
NSMutableArray *constraints = UIInterfaceOrientationIsLandscape(interfaceOrientation) ? self.landscapeConstraints : self.portraitConstraints;
[self.scrollView removeConstraints:self.scrollView.constraints];
[self.scrollView addConstraints:constraints];
[UIView animateWithDuration:duration
animations:^{
[self.scrollView layoutIfNeeded];
}];
As far as I know (and as far as my research goes), this is just how it should be done... but obviously I am wrong and I cannot for the life of me figure out what I need to do differently. At the end of the animation everything is positioned perfectly fine, but the subviews simply refuse to animate. Any help is greatly appreciated!
For the sake of completeness it might be worth mentioning that I load the views that I position within the scroll view from a storyboard where I also set the constraints for their subviews. I think this should not be a problem though, since the layout actually works... except for the animations.

how to animate views change views hierarchy using autolayout?

I have superview called self.myMainView
In that view I have three subviews: UIView *view1 (green view), UIView *view2 (grey view), UIImageView *view3
I want to animation moving of view 3
and when view 3 is finished this animation than I want to change view hierarchy, like on example screenshots.
Here is example images:
What you are doing is the normal, standard way to change the layering order of views. So the problem is not this, but in your words "But, it violates my animation" and "crushing my animation". The problem, then, is with your animation, if it can be "violated" and "crushed" by rearranging the layers. However, you do not provide, in your question, any information about the animation! Yet that is clearly the heart of the problem.
EDIT: Now that you've posted the animation code, I was able to test, and I see what the problem is. The problem is that you are using autolayout.
view3 is positioned by constraints. When you animate the position of view3, you violate those constraints, but this does not become immediately evident. But when you exchange the layering order of view1 and view2, layout is performed! The constraints on view3 are then enforced. Since those constraints did not change, we see view3 back where you originally had it.
The simplest solution is to turn off autolayout if you don't need it. Otherwise, you will have to change the constraints, after the animation or as part of the animation. In fact, you can reposition view3 by animating its positional constraints. I describe all this in my book, here: http://www.apeth.com/iOSBook/ch17.html#_animation_and_autolayout
If all you want to do is switch which of your 2 subviews are on top of one another, can you simply use [UIView bringSubviewToFront:] rather than directly messing with the order of your subview array? That might solve the animation problems. The other thing I would try is put an if( finished ) around your call to exchangeSubviewAtIndex to make sure it doesn't interfere with the other views being animated.
The problem was in the constraints of view 3 which doesn't change after animation and when we wants to manipulate the view hierarchy, animation is stopped work properly. Matt said about that in his answer, also he provide a solution from his great book: http://www.apeth.com/iOSBook/ch17.html#_animation_and_autolayout
thank you for that Matt.
I change this solution a little and decide to post it here for anyone who will need this. But if you want to understand this better I highly recommend you to read chapter about animation from book, link is above.
First, we need to animate our movement of view 3, then change our constraints and only after this change the view hierarchy. In the book a found several ways to done this, but this one is seemed to me the best:
layout = -35.0; // new constraint constant of self.view3
NSArray *constraintsForAnimation = self.myMainView.constraints; // all constraints of superview
NSUInteger indexOfconstraintsAttribute = [constraintsForAnimation
indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop){ // get index of constraint of view 3
NSLayoutConstraint *constraint = obj;
return (([constraint.firstItem isEqual: self.view3])) ? idx : NO; // return this index if we found constraint of view 3
}];
constraint.constant = layout; // changing the constraint of view3
[UIView animateWithDuration:0.3 animations:^{ // animation
[self.view3 layoutIfNeeded]; // apply new constraints to view3
}];
[self.myMainView exchangeSubviewAtIndex:3 withSubviewAtIndex:4];// toggle view 1 with view 2
We move our view 3 only with help of constraints, we don't set frame or centre view property.
Okay, we done with and this working like as I want. Hope this help you too.

Passing swipe touches from UIView to underlying UIScrollView for proper scrolling

I have a situation similar to these two posts (1907297 AND 689684) and to describe my situation most concisely, I present this text/graphical layout (similar to what you'd see in IB, dots used to enforce indent levels)
UIView (MainView: 320x460)
. .UIScrollView (ScrollView: 320x460)
. .UIView (OverlayView: 320x40)
. . . .UIButton (ArbitraryButton1)
. . . .UILabel (ArbitraryLabel1)
. . . .UILabel (ArbitraryLabel2)
The goal here is for the OverlayView to serve as a unified, transparent container to position and display some arbitrary buttons/labels on top of the ScrollView. These buttons/labels should remain stationary while the content in the ScrollView beneath moves with user swipes. The buttons/labels may sometimes be hidden/unhidden/scaled in unison (with animation) which is what makes it handy to have them all grouped in the single OverlayView.
The trouble is that, while taps on the OverlayView seem to transmit right through to the underlying ScrollView just nicely, swiping motions have no effect. I can detect/intercept the swipes by overriding the
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
method in the OverlayView, however I haven't yet found a way to properly pass those along to the ScrollView in a way that makes it scroll. Evidently the touchesMoved method is not what UIScrollView uses to detect/interpret swipes?
All the other similar posts I've researched have either found a different solution that wouldn't work in my case or have just gone unsolved. I've also seen mention of employing touchesShouldBegin / touchesShouldCancel though I don't grasp how that would be implemented. Anyhow, still hopeful that there's some insight from the community that can allow me to come up with an elegant solution for this - any sample code would be fantastic.
Thanks in advance, Joel.
P.S. - I should also mention that I need to make this compatible with iOS 3.0 so I think trying to use UIGestureRecognizers is out.
Here's an easier solutions that worked well for me:
In the OverlayView (the view on top of UIScrollView), make the width and height both 0 (so that the view is no longer technically on top of the scrollview) and set clipsToBounds = NO (so that the contents of the OverlayView still show up on top of the scrollview). It worked like a charm for me.
self.OverlayView.clipsToBounds = NO;
CGRect frame = self.OverlayView.frame;
self.OverlayView.frame = CGRectMake(frame.origin.x, frame.origin.y, 0, 0);
Note that if OverlayView contains interactive controls (like the button above) then they will no longer work. You'll need to move it into it's own view above the UIScrollView.
How about, at runtime in viewDidLoad you take the buttons out of the container view and place them in the view as subviews directly (and get rid of the container view)? Then there's no container view to intercept swipes but you can still use a view to group things in IB.
Also potentially you could put the container view in as a subview of the scroll view instead, and in the scroll view delegate keep re-positioning the view whenever the user scrolls. That would seem to have a high potential for being jittery but may be worth a try.
Also if the containing view is a visual container and you need to see it, you could instead use a CALayer that was placed in the superview on top of the CALayer for rendering the scroll view, since CALayers have nothing to do with input and would not each touches.
You should subclass UIScrollView and override the touchesShouldCancelInContentView: method
-(BOOL)touchesShouldCancelInContentView:(UIView *)view
{
if ([view isKindOfClass:[UIButton class]]) {//or whatever class you want to be able to scroll in
return YES;
}
if ([view isKindOfClass:[UIControl class]]) {
return NO;
}
return YES;
}

Resources