How to keep UIView at relative distance from other after resizing? - objective-c

I have five UIView on a UIScrollView. All of them with the same width. Each view has other subviews that resize its height according to the content assigned, thus making the parent UIView and the UIScrollView resizable as well. I am trying to keep the 5 UIView separated from each other at a certain "Padding" distance even after resizing. What I do right now is set the position of the origin.y and the height of each UIView when layoutSubviews is called. Is there an easier way to do this?
I have tried to set their position on creation like: CGRectMake(0, aboveView.frame.origin.y + aboveView.frame.size.height + Padding, width, 0) and setting its autoresizingMask to UIViewAutoresizingMaskTopMargin. Hoping that when I call sizeToFit on the main UIView, all the UView will set their positions relative to the view above them.

Overriding layoutSubviews is the right way to do this. UIKit doesn't have any built-in layout management that can do it for you.
However, you might not realize that UIScrollView sends itself layoutSubviews each time it scrolls - on every frame of the scrolling. That may be a lot more often than you need! You don't want to do a lot of work in a UIScrollView's layoutSubviews if you can avoid it.
To avoid doing extra layout, I suggest you set up your view hierarchy like this:
UIScrollView
ContainerView with layoutSubviews method
content view 1
content view 2
content view 3
content view 4
content view 5
Use a standard UIScrollView. Give it one subview, which is a custom UIView subclass (I called it ContainerView in my example). The ContainerView has your five content views as its subviews.
When you assign new content to one of your five content views, send sizeToFit to that content view. If the view's size changes, UIKit should automatically send layoutSubviews to its superview - the ContainerView. The ContainerView's layoutSubviews method adjusts the position of its subviews to maintain the padding between them, and then sets the contentSize of its parent - the UIScrollView.
- (void)layoutSubviews {
CGRect myFrame = CGRectZero;
for (UIView *subview in self.subviews) {
CGRect frame = subview.frame;
if (myFrame.size.height > 0) {
frame.origin.y = myBounds.size.height + Padding;
subview.frame = frame;
}
myFrame = CGRectUnion(myFrame, frame);
}
self.frame = myFrame;
UIScrollView *scrollView = self.superview;
scrollView.contentSize = myFrame.size;
}
This way, you don't do any extra work just because the scroll view scrolled. You only lay out your content views when the content actually changes.

Related

How to prevent certain object from scrolling in a UIScrollView

I don't know if this is possible and I highly doubt that it is but I'm wondering if there's a way where I can prevent a button for example from scrolling in a UIScrollView using Objective-C programming?
Sure. Simply update the button's origin as the scroll view scrolls.
In your view controller, implement the appropriate scroll view delegate method. If not done already, setup your view controller as the scroll view's delegate.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint offset = scrollView.contentOffset;
CGRect frame = self.fixedButton.frame;
frame.origin.y = offset.y + 40;
self.fixedButton.frame = frame;
}
This will keep the self.fixedButton button 40 points below the top of the visible portion of the scroll view. Adjust as needed.
The above all assumes the button is a subview of the scroll view.
Of course it may be a lot easier if the button and the scroll view share a common parent view. Then the button isn't a subview of the scroll view and won't scroll at all.

Force Subview to fill parent

I have some subviews that I place inside each of my tab bar's view controllers. Right now I'm sizing it with a pixel count but it only works on the iPhone 4 and 4s and not the iPhone 5 because of the longer screen size. I could check for the device and then size it that way but I feel like there has to be an easier way to do this.
viewController1.view.frame = CGRectMake(0, 0, 320, 460);
I colored the subview yellow so it's easier to see.
You should NOT change frame of tabbar's content view controller's view. UITabBar takes itself care of sizing the child view controller's frame properly.
If you want to add subview to content view controller (controller under some tab) and make that view to always automatically resize with the controllers main view (self.view), you can use combination of superviews frame and autoresizing.
Example code (you can do this in - (void)viewDidLoad for example):
UIView *view = [[UIView alloc] initWithFrame:self.view.bounds];
view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:view];
If you want to do this by setting frame than DO this:
[childView setFrame:childView.superview.bounds];
The master view in your view controller should already be the size of the usable space on the sceen. In general, if you want a view to be the same size as it's parent, you can use view.frame = view.superview.frame, though I doubt that would be a good idea to call on the view controller's view.

UIScrollView contentSize not working

I put a UIScrollView in my nib's view, and linked it to a an IBOutlet property.
Now, when I do this in my viewDidLoad method, it seems to have no effect on the contentSize:
self.sv.backgroundColor = [UIColor yellowColor]; // this works
CGSize size = CGSizeMake(1000.0, 1000.0);
[self.sv setContentSize:size]; // this does not
It behaves as if the contentSize was the same as the frame. What's going on?
This started working when I turned off AutoLayout. Why?
I had the same problem. Auto Layout for UIScrollView is messed up.
Work around: Put everything in the UIScrollView into another UIView, and put that UIView as the only child of the UIScrollView. Then you can use Auto Layout.
If things near the end is messed up (the end of whichever direction your UIScrollView scrolls), change the constraint at the end to have the lowest possible priority.
I tried viewWillLayoutSubviews to update scrollView's contentSize, it worked for me.
- (void)viewDidLayoutSubviews
{
[self.bgScrollView setContentSize:CGSizeMake(320, self.view.frame.size.height* 1.5)];
}
Apple Doc
-(void)viewDidLayoutSubviews
Called to notify the view controller that its view has just laid out its subviews.
Discussion
When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view’s subviews have been adjusted. Each subview is responsible for adjusting its own layout.
Your view controller can override this method to make changes after the view lays out its subviews. The default implementation of this method does nothing.
The easiest/cleanest way is to set contentSize at viewDidAppear so you negate the effects of autolayout. This doesn't involve adding random views. However relying on load order for an implementation to work may not be the best idea.
Use this code. ScrollView setContentSize should be called async in main thread.
Swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
DispatchQueue.main.async {
var contentRect = CGRect.zero
for view in self.scrollView.subviews {
contentRect = contentRect.union(view.frame)
}
self.scrollView.contentSize = contentRect.size
}
}
Objective C:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
dispatch_async(dispatch_get_main_queue(), ^ {
CGRect contentRect = CGRectZero;
for(UIView *view in scrollView.subviews)
contentRect = CGRectUnion(contentRect,view.frame);
scrollView.contentSize = contentRect.size;
});
}
There are two problems here. (1) viewDidLoad is too soon; you have to wait until after layout has taken place. (2) If you want to use autolayout with a scrollview that comes from a nib, then either you must use constraints to completely describe the size of the contentSize (and then you don't set the contentSize in code at all), or, if you want to set it in code, you must prevent the constraints on the scrollview's subviews from dictating the contentSize. It sounds like you would like to do the latter. To do so, you need a UIView that acts as the sole top-level subview of the scrollview, and in code you must set it to not use autolayout, enabling its autoresizingMask and removing its other external constraints. I show an example of how to do that, here:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch20p573scrollViewAutoLayout/ch20p573scrollViewAutoLayout/ViewController.m
But notice also the next example, which shows how to use constraints completely, instead of contentSize.
A SUPER easy way to use AutoLayout with UIScrollViews inside Interface Builder:
Step 1: Create a UIScrollView
Step 2: Create a UIView that is a child of your scroll view like so:
-UIScrollView
---UIView
-----Your other content
(We'll call this one contentView).
Step 3: In the size inspector, give this view a height and width (say, 320x700).
Step 4 (using AutoLayout): Create unambiguous constraints from your contentView to its superview (the UIScrollView): connect the 4 edges (top, leading, trailing, bottom), then give it a defined width and height that you want it to scroll too.
For example: If your scroll view spans the entire screen, you could give your content view a width of [device width] and a height of 600; it will then set the content size of the UIScrollView to match.
OR:
Step 4 (not using AutoLayout): Connect both of these new controls to your view controller using IB (ctrl+drag from each control to your view controller's .h #implementation). Let's assume each is called scrollView and contentView, respectively. It should look like this:
#interface YourViewController : UIViewController
#property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
#property (strong, nonatomic) IBOutlet UIView *contentView;
#end
Step 5 (not using AutoLayout): In the view controller's .h file add (actually, override) the following method:
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.scrollView.contentSize = self.contentView.frame.size;
}
You can use this lines of code into your *.m file's
- (void)viewDidLoad{
[scroll setContentSize:CGSizeMake(320, 800)] ;
[scroll setScrollEnabled:TRUE];
[scroll setShowsVerticalScrollIndicator:NO];
[scroll setShowsHorizontalScrollIndicator:YES];
}
for this you need to take an IBOutlet property of UIScrollView into your *.h file this way:
IBOutlet UIScrollView *scroll;
And connect this from Storyboard.
Or,
You can use this method into your *.m file:
-(void)viewDidLayoutSubviews
{
[scroll setContentSize:CGSizeMake(320, self.view.frame.size.height* 1.5)];
// this will pick height automatically from device's height and multiply it with 1.5
}
This both solution works for me in xcode-5, xcode-6, xcode-6.1, xcode-6.2
Setting the contentSize in viewDidAppear is critical.
But I also had a variation of what worked in the 3.5 inch screen, and the 4 inch screen. The 4 inch screen worked, the older one does not. Both iOS 7. Bizarre is an understatement!
I could never get auto layout based on constraints to work. Since my view was already a subclass UIScrollView I solved it by overriding setContentView: and ignoring auto layouts zero height setContentSize: message.
#interface MyView : UIScrollView {}
#end
#implementation MyView
- (void)setContentSize:(CGSize)aSize {
if (aSize.height > 0)
[super setContentSize:aSize];
}
#end
I used to do set up the uiscrollview programmatically UNTIL I watched the following wonderful tutorial, step by step how to get uiscrollview and uiview to work: https://www.youtube.com/watch?v=PgeNPRBrB18
After watching the video you will start liking Interface Builder I am sure.
Vote up
Still not scrolling when dynamic height of labels exceeds view height.
I did what yuf's answer marked as correct above said to do (I added a content view to my scrollview and set the constraints leading, trailing, top bottom, and equal widths from the content view to the scroll view.) but still my view was not scrolling when the internal controls height exceeded the height of the scrollview.
Inside my content view I have an image and 3 labels below it. Each label adjusts their own height dependant on how much text is in them (they are set to word-wrap and numberoflines = 0 to achieve this).
The problem I had was my content view's height was not adjusting with the dynamic height of the labels when they exceeded the height of the scroll view/main view.
To fix this I worded out I needed to set the Bottom Space to Container constraint between my bottom label and the contentview and gave it a value of 40 (chosen arbitrarily to give it a nice margin at the bottom). This now means that my contentview adjusts its height so that there is a space between the bottom of the last label and itself and it scrolls perfectly!
Yay!
Try this out...
add all constraints like you do for UIView (See screenShot of my ViewControler in Storyboard)
Now trick begins. select your last object and select its bottom constraint. (See above screenShot, Instagram button's Bottom Constraint(Yellow line)) and Change the Constant in Size Inspector like in bellow screenshot.
i require Constant=8 but you can change as per your requirements.
this Constant is the Space between That Orange Button's Bottom and the scrollView.
EDIT
Make Sure about your view's hierarchy .
0) ViewController.view (optional)
1) UIScrollView
2) UIView (Rename as "contentView")
3) UIView (this view is your content that will make scrollView scroll)
I finally worked out my own solution to this problem because in my case I couldn't use the view controller's life cycle. Create your own scroll view subclass and use it instead of UIScrollView. This even worked for a scroll view inside a collection view cell.
class MyScrollView:UIScrollView {
var myContentSize:CGSize = CGSize.zero // you must set this yourself
override func layoutSubviews() {
super.layoutSubviews()
contentSize = myContentSize
}
}
My MyScrollView was defined in the nib with a tag of 90. If so this is a good way to set content size in the code in the parent view.
let scrollView = viewWithTag(90) as! MyScrollView
scrollView.myContentSize = ...
If you are using AutoLayout a really easy way to set the contentSize of a UIScrollView is just to add something like this:
CGFloat contentWidth = YOUR_CONTENT_WIDTH;
NSLayoutConstraint *constraintWidth =
[NSLayoutConstraint constraintWithItem:self.scrollView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeLeading
multiplier:1
constant:contentWidth];
[self.scrollView addConstraint:constraintWidth];
I got Autolayout to work for paginated scroll views whose pages occupy the full-width of the screen. The pages automatically resize according to the scroll view's size. I haven't tested this for lesser-width scroll views but do comment away if it works--I beleieve it should. Targeted for iOS 9, wrote code in Swift 2, used a mix of IB's and custom code in awakeFromNib.
Steps:
Define a full-screen scroll view.
Inside the scroll view, add a UIView (I called mine contentView) whose top, trailing, bottom, and leading edges to the scroll view are all zero; the height is equal to the scroll view's; but the width is the scroll view's width times the number of pages. If you're doing this visually, you will see your content view extend beyond your scroll view in Inteface Builder.
For every "page" inside the contentView, add Autolayout rules to put them side-by-side each other, but most importantly, give them each a constraint so that their widths are equal to the scroll view's, not the content view's.
Sample code below. embedChildViewController is just my convenience method for adding child VCs--do look at setupLayoutRulesForPages. I have exactly two pages so the function is too simple, but you can expand it to your needs.
In my view controller:
override func loadView() {
self.view = self.customView
}
override func viewDidLoad() {
super.viewDidLoad()
self.embedChildViewController(self.addExpenseVC, toView: self.customView.contentView, fillSuperview: false)
self.embedChildViewController(self.addCategoryVC, toView: self.customView.contentView, fillSuperview: false)
self.customView.setupLayoutRulesForPages(self.addExpenseVC.view, secondPage: self.addCategoryVC.view)
}
My custom view:
class __AMVCView: UIView {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var contentView: UIView!
#IBOutlet weak var pageControl: UIPageControl!
override func awakeFromNib() {
super.awakeFromNib()
self.scrollView.pagingEnabled = true
self.scrollView.bounces = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.pageControl.numberOfPages = 2
self.contentView.backgroundColor = UIColor.blueColor()
self.scrollView.backgroundColor = UIColor.clearColor()
self.backgroundColor = UIColor.blackColor()
}
func setupLayoutRulesForPages(firstPage: UIView, secondPage: UIView) {
guard self.contentView.subviews.contains(firstPage) && self.contentView.subviews.contains(secondPage)
else {
return
}
let rules = [
"H:|-0-[firstPage]-0-[secondPage]-0-|",
"V:|-0-[firstPage]-0-|",
"V:|-0-[secondPage]-0-|"
]
let views = [
"firstPage" : firstPage,
"secondPage" : secondPage
]
let constraints = NSLayoutConstraint.constraintsWithVisualFormatArray(rules, metrics: nil, views: views)
UIView.disableAutoresizingMasksInViews(firstPage, secondPage)
self.addConstraints(constraints)
// Add the width Autolayout rules to the pages.
let widthConstraint = NSLayoutConstraint(item: firstPage, attribute: .Width, relatedBy: .Equal, toItem: self.scrollView, attribute: .Width, multiplier: 1, constant: 0)
self.addConstraint(widthConstraint)
}
}

Resize parent view if subview changes size

I'm new to Cocoa and I'm programming a custom InspectorView.
A parent view (InspectorView) contains several subviews (InspectorCategories).
If I uncollapse a category (subview) I have to resize/relayout my parents view?
I found out that this is not possible through autoresize masks - Is this correct?
I tried it with resizeSubviewsWithOldSize in my parents view but this gets not called while resizing the subview.
How can I achieve this behavior?
There are two parts to accomplish what I think you want:
(a) In the parent view, override the sizeThatFits: method so that it computes a new size that fits around the resized subview.
(b) In the subview, override the setFrame: method and after the frame size is changed, it calls [self.superview sizeToFit] to resize the superview, perhaps like this:
-(void)setFrame:(CGRect)newFrame
{
[super setFrame:newFrame];
[self.superview sizeToFit];
}
No, it is not possible to do through autoresizing masks. Autoresizing mask defines how the view is resized when its superview changes bounds.
The subview should let its superview know what size it needs, for example through a delegate call. The superview then should resize itself and the subview.

Resizing UIView with UIScrollView subview

I have a big problem concerning the resizing of a derived UIView with a UIScrollView as subview.
In the layoutSubviews message I set the frame of the UIScrollView subview. the UIScrollView contains a UIImageView with a big image which can be moved/pinched and so on. The initial contenSize of the UIScrollView is the initial image size.
So far, so good. Moving and pinching of the image works well. Now I have to change the UIView frame (in my app to maximize the UIView). im doing that in a animation block (beginAnimations/commitAnimations). So I set the new frame (which will update the width & height) an then I call [myView layoutIfNeeded] to force the UIScrollView to update its frame in the layoutSubviews message of my view.
The UIView animates correct to its new frame and if the contentOffset of the UIScrollView is currently x 0, y 0 the UIScrollView frame will be updated properly. but here's my problem: if the contentOffset of the UIScrollView is bigger than x 0, y 0 the UIScrollView will "slide in" from upper left to its final position.
I want that the UIScrollView resizes its frame properly with the parents frame and aligns the content (in my case the UIImageView) right. But how could I achieve that?
after hours of web research i found the solution for this problem:
[UIView setAnimationBeginsFromCurrentState: YES];
this will animate all layers from the current state.