Add constraints based on view's height iOS - ios

I have several subviews which are laid out based on the size of their super view. And I use auto layout here but the size of the super view is always 0, here is the code:
- (instancetype)initWithFrame:(CGRect)frame Count:(NSUInteger)count
{
self = [super initWithFrame:frame];
if (self)
{
self.count = count;
const float circleHeight = self.bounds.size.height * (float)4 / (5 * self.count - 1);
NSLog(#"selfHeight %f",self.bounds.size.height);
for (UIImageView * circle in self.circleViewArray)
{
[self addSubview:circle];
}
for (int i = 0;i < self.count;i++)
{
[self.circleViewArray[i] mas_makeConstraints:^(MASConstraintMaker * make){
make.width.equalTo(self);
make.height.equalTo(self).multipliedBy((float)4 / (5 * self.count - 1));
make.centerX.equalTo(self);
make.bottom.equalTo(self).with.offset(-i * (circleHeight + stickHeight));
}];
}
}
return self;
}
Note that here I use the third-party Masonry to simplify my code.
When I print the "selfHeight" in the console the output is always 0. How should I handle it?

So your offsets are never going to work because they're calculated against zero and never updated. You need to make the constraints relative somehow, or you need to remove and update the constraints each time the frame changes (the layout needs to be updated).
Making the constraints relative is better, which in your case you should look at linking the views together so the spacing between yhen is set and the heights resize to fit the full available height.
Pseudocode:
UIView *previousView = nil;
for (view in views) {
view.leading = super.leading;
view.trailing = super.trailing;
if (previousView) {
view.top = previousView.bottom;
view.height = previousView.height; // equal relation, not static height
} else {
view.top = super.top;
}
previousView = view;
}
previousView.bottom = super.bottom;

Related

How to update size constraint of view based on size of superview using Auto Layout?

I want self.boardView, which is a subview of the controller's view, to be sized to the largest square that fits inside its superview. I set constraints in IB so that self.boardView is centered, and I have outlets for the width and height constraints. I am doing the following in the controller:
-(void)updateViewConstraints {
[super updateViewConstraints];
const CGFloat W = self.view.bounds.size.width;
const CGFloat H = self.view.bounds.size.height;
const CGFloat S = (W < H) ? W : H;
self.boardViewWidthConstraint.constant = S;
self.boardViewHeightConstraint.constant = S;
}
The problem is that the controller's view bounds are not necessarily up to date (e.g., after a rotation). How/where can I set the constraints above based on the new size of the superview?
This may be a "chicken and egg" problem since the view sizes can't be updated until the new constraints are set, but I can't compute the constraints until the view sizes are updated.
There are apparently changes in iOS8 how updateViewConstraints is called, see Behavior changes for updateViewConstraints in iOS 8.
I suggest trying
-(void)layoutSubviews {
[super layoutSubviews];
const CGFloat W = self.view.bounds.size.width;
const CGFloat H = self.view.bounds.size.height;
const CGFloat S = (W < H) ? W : H;
self.boardViewWidthConstraint.constant = S;
self.boardViewHeightConstraint.constant = S;
}

using Autolayout maintain ratio in storyboard just like group and stretch work in ms word

I would like to achieve which I'll explain with an example described as follow.
In MS Word, in a canvas, we can add several objects like arrow, circle, square etc. Then we can easily group them. Once objects are properly grouped, now matter how we scale, it is scaled as a group & gives us a perfect scaling. I wish to achieve same using Autolayout in Storyboard / xib. According to me all objects scales based on their individual constraints.
Here is an example image.
Here is the structure of above image.
View with white color background
View with gray color background
View with white color bg and on top-center
View with white color bg and in center-center
View with white bg and bottom left
View with white bg and bottom right
If the top-most parent view is scaled down, every other elements in it recursively should get scaled down.
How to achieve such functionality using Autolayout?
Edit:
Here - I've prepared a quick gif for the same.
You can do this by using the multiplier part of the constraint system.
Each constraint is just an equation...
calculatedSize = multiplier * inputSize + constant
The calculatedSize is the size of your view or the distance from the edge etc...
The inputSize (when used) is the size of another view when (for example) using equal widths etc...
In order to get the behaviour you are after you need the calculatedSize to always be a multiple of the inputSize and have the constant set to 0.
So, in your example animation. If the grey square is set to have a width equal to the group width * 0.5 then it will always scale properly.
That is fine for the sizes. The gaps can't work like this though.
In order to get the gaps to scale properly you can't just have them as gaps. Gaps are not "objects" in the language of AutoLayout. You can't make one gap equal to another. Nor can you make a gap related to the width of another view.
What you can do though is replace the gaps with "spacer" views. These are actual UIViews that you add to your storyboard but then you make them hidden so that you can't see them at runtime.
So, going back to the grey square in the example you might have something like...
|[spacerView][greySquare]
Now set the width of the spacerView to be superView.width * 0.17 and the width of the greySquare to be superView.width * 0.5.
// using the equation above
greySquare.width = superView.width * 0.5 + 0
spacerView.width = superView.width * 0.17 + 0
You can set the spacerView to hidden either in Interface Builder (this will hide it at design time) or in a runtime attribute (this will hide it at runtime).
Now when you resize the superView the spacer will grow relative to the amount you have grown the superView.
This can get quite complex though as you still need to make sure you have all the spacer views constrained properly.
I found a solution for this problem but maybe is not the best. The idea is to change the Width and Height of the root view, calculate the percentage of Width and Height variation from the initial state and apply this to all children constraint.
In your Storyboard create a root UIView and place all UIView children inside.
You need to set 4 constraints per child and for the root View: Width, Height, Vertical space and Horizontal space.
Connect the root view, width constraint and height constraint to the UIViewController.
Code
#import "ViewController.h"
#interface ViewController ()
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *heightConstraint;
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *widthConstraint;
#property (strong, nonatomic) IBOutlet UIView *rootView;
#end
#implementation ViewController{
CGPoint dragPoint;
CGFloat startDragWidth;
CGFloat startDragHeight;
NSMutableDictionary *startDragConstraints;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(detectPan:)];
self.rootView.gestureRecognizers = #[panRecognizer];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
startDragHeight = self.heightConstraint.constant;
startDragWidth = self.widthConstraint.constant;
startDragConstraints = [NSMutableDictionary dictionary];
//I used i and j for setting an identifier on the constraint
for (int i = 0; i < [self.rootView.subviews count]; i++)
{
UIView *view = self.rootView.subviews[i];
//Looping childs Width and Height constraints
for(int j = 0; j < [view.constraints count]; j++)
{
NSLayoutConstraint *constraint = view.constraints[j];
constraint.identifier = [NSString stringWithFormat:#"%d%d", i, j];
[startDragConstraints setObject:[NSNumber numberWithFloat:constraint.constant] forKey:constraint.identifier];
}
}
//Looping childs Vertical space and Horizontal space constraints. They live in the parent view
for (int i = 0; i < [self.rootView.constraints count]; i++)
{
NSLayoutConstraint *constraint = self.rootView.constraints[i];
constraint.identifier = [NSString stringWithFormat:#"%d", i];
[startDragConstraints setObject:[NSNumber numberWithFloat:constraint.constant] forKey:constraint.identifier];
}
}
- (void) detectPan:(UIPanGestureRecognizer *) uiPanGestureRecognizer
{
dragPoint = [uiPanGestureRecognizer translationInView:self.rootView];
if(fabsf(dragPoint.x) > fabsf(dragPoint.y))
{
dragPoint.y = dragPoint.x;
}
else
{
dragPoint.x = dragPoint.y;
}
self.heightConstraint.constant = startDragHeight + dragPoint.y;
self.widthConstraint.constant = startDragWidth + dragPoint.x;
CGFloat heightDelta = (self.heightConstraint.constant - startDragHeight) / startDragHeight;
CGFloat widthDelta = (self.widthConstraint.constant - startDragWidth) / startDragWidth;
for(UIView *view in self.rootView.subviews)
{
for(NSLayoutConstraint *constraint in view.constraints)
{
float previousConstant = [[startDragConstraints objectForKey:constraint.identifier] floatValue];
if(constraint.firstAttribute == NSLayoutAttributeWidth) //Width constraint
{
constraint.constant = previousConstant + previousConstant * widthDelta;
}
else if(constraint.firstAttribute == NSLayoutAttributeHeight) //Height constraint
{
constraint.constant = previousConstant + previousConstant * heightDelta;
}
}
}
for(NSLayoutConstraint *constraint in self.rootView.constraints)
{
float previousConstant = [[startDragConstraints objectForKey:constraint.identifier] floatValue];
if(constraint.firstAttribute == NSLayoutAttributeLeading) //Horizontal space constraint
{
constraint.constant = previousConstant + previousConstant * widthDelta;
}
else if(constraint.firstAttribute == NSLayoutAttributeTop) //Vertical space constraint
{
constraint.constant = previousConstant + previousConstant * heightDelta;
}
}
}
#end

How do I position my UIScrollView's image properly when switching orientation?

I'm having a lot of trouble figuring out how best to reposition my UIScrollView's image view (I have a gallery kind of app going right now, similar to Photos.app, specifically when you're viewing a single image) when the orientation switches from portrait to landscape or vice-versa.
I know my best bet is to manipulate the contentOffset property, but I'm not sure what it should be changed to.
I've played around a lot, and it seems like for whatever reason 128 works really well. In my viewWillLayoutSubviews method for my view controller I have:
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.x >= 128) {
newContentOffset.x -= 128.0;
}
else {
newContentOffset.x = 0.0;
}
newContentOffset.y += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
else {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.y >= 128) {
newContentOffset.y -= 128.0;
}
else {
newContentOffset.y = 0.0;
}
newContentOffset.x += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
And it works pretty well. I hate how it's using a magic number though, and I have no idea where this would come from.
Also, whenever I zoom the image I have it set to stay centred (just like Photos.app does):
- (void)centerScrollViewContent {
// Keep image view centered as user zooms
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(self.scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(self.scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(self.scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(self.scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
}
So I need it to keep it positioned properly so it doesn't show black borders around the image when repositioned. (That's what the checks in the first block of code are for.)
Basically, I'm curious how to implement functionality like in Photos.app, where on rotate the scrollview intelligently repositions the content so that the middle of the visible content before the rotation is the same post-rotation, so it feels continuous.
You should change the UIScrollView's contentOffset property whenever the scrollView is layouting its subviews after its bounds value has been changed. Then when the interface orientation will be changed, UIScrollView's bounds will be changed accordingly updating the contentOffset.
To make things "right" you should subclass UIScrollView and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
The contentOffset calculation function should be placed inside UIScrollView's layoutSubviews method. The problem is that this method is called not only when the bounds value is changed but also when srollView is zoomed or scrolled. So the bounds value should be tracked to hint if the layoutSubviews method is called due to a change in bounds as a consequence of the orientation change, or due to a pan or pinch gesture.
So the first part of the UIScrollView subclass should look like this:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews method is called every time the UIScrollView is panned, zoomed or its bounds are changed. The _centerScrollViewContent method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView class and therefore instead of using self.imageView property to reference the zoomed view, which may not be available in the context of UIScrollView class, the viewForZoomingInScrollView: delegate method is used.
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset method. This method is responsible for adjusting the contentOffset. Such that when UIScrollView's bounds value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView's bounds is changed (e.g.: orientation change).
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView class (here is link to GitHub) implementing the above behavior and additional bonuses:
You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
In addition to adjusting contentOffset you may find that you also want to adjust the scrollView's zoomScale. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.

Horizontally center multiple UIViews

I want to horizontally center a number of UIViews (they happen to be circles) in the master UIView. It will end up basically looking like the dots on the standard Page Control.
I have all the code written to create the circle UIViews I just have no idea how to arrange them horizontally and dynamically at run time.
Essentially I need some kind of horizontal container where I can do this
-(void)addCircle{
[self addSubView:[CircleView init]];
}
And it will auto arrange however many children it has in the center.
I get confused with auto-layout as well from time to time but here is a way how you can do it programmatically: (I assume that you add your circle views to a containerView property of your view controller and you do not add any other views to it.)
Add these two properties to your view controller:
#property (nonatomic) CGRect circleViewFrame;
#property (nonatomic) CGFloat delta;
Initiate those properties with the desired values in your view controller's viewDidLoad method:
// the size (frame) of your circle views
self.circleViewFrame = CGRectMake(0, 0, 10, 10);
// the horizontal distance between your circle views
self.delta = 10.0;
Now we add your "automatic addCircle method":
- (void)addCircleView {
UIView *newCircleView = [self createCircleView];
[self.containerView addSubview:newCircleView];
[self alignCircleViews];
}
Of course we need to implement the createCircleView method...
- (UIView*)createCircleView {
// Create your circle view here - I use a simple square view as an example
UIView *circleView = [[UIView alloc] initWithFrame:self.circleViewFrame];
// Set the backgroundColor to some solid color so you can see the view :)
circleView.backgroundColor = [UIColor redColor];
return circleView;
}
... and the alignCircleViews method:
- (void)alignCircleViews {
int numberOfSubviews = [self.containerView.subviews count];
CGFloat totalWidth = (numberOfSubviews * self.circleViewFrame.size.width) + (numberOfSubviews - 1) * self.delta;
CGFloat x = (self.containerView.frame.size.width / 2) - (totalWidth / 2);
for (int i = 0; i < numberOfSubviews; i++) {
UIView *circleView = self.containerView.subviews[i];
circleView.frame = CGRectMake(x,
self.circleViewFrame.origin.y,
self.circleViewFrame.size.width,
self.circleViewFrame.size.height);
x += self.circleViewFrame.size.width + self.delta;
}
}
This is the most important method which will automatically realign all your subviews each time a new circleView is added. The result will look like this:
Simple steps: append circle to container view, resize container view, center align container view
-(void)addToContanerView:(CircleView*)circle{
circle.rect.frame = CGrectMake(containers_end,container_y,no_change,no_change);
[containerView addSubview:circle];
[containerView sizeToFit];
containerView.center = self.view.center;
}
Assumptions:
containers_end & containers_y you can get from CGRectMax function,
for UIView SizeToFit method check here
To take care of rotation use make sure your Autoresizing subviews are set for left, right bottom and top margin.
You can try using this library. I have used it on several of my projects and so far, it worked really well.
https://github.com/davamale/DMHorizontalView

how to add custom layers to UITableViewCell in iOS6

I have a prototype cell with a custom class that I want to draw a few extra layers on the cell once when the cell is first initialized and not every time the cell is reused. In the past I would have done this by implementing awakeFromNib. I want to be able to access the frame of the views in my cell so I can use their dimensions in my new layer drawings, but with iOS6 the subviews all have frame width/height of 0 in the awakeFromNib method. I suspect it has to do with the new constraints layout stuff which I don't really understand yet.
- (void)awakeFromNib {
[super awakeFromNib];
// We only want to draw this dotted line once
CGPoint start = CGPointZero;
CGPoint end = CGPointMake(self.horizontalSeparator.frame.size.width, 0);
// Category that creates a layer with a dotted line and adds it to the view.
[self.horizontalSeparator addDottedLine:start to:end];
}
In awakeFromNib the horizontalSeparator.frame = (0 100; 0 0). How can I draw this dotted line layer once per cell and use the width of the existing horizontalSeparator view to determine the length of the line?
UPDATE
I figured out that I can use the constraints on the superview to figure out the dimensions on the subviews, but I'm still hoping someone can point me towards a better solution that doesn't make assumptions about the constraint configuration.
for (NSLayoutConstraint *constraint in self.constraints) {
if (// Find a constraint for the horizontalSeparator
(constraint.firstItem == self.horizontalSeparator
|| constraint.secondItem == self.horizontalSeparator)
&& // Make sure it affects the leading or trailing edge.
(constraint.firstAttribute == NSLayoutAttributeLeading
|| constraint.firstAttribute == NSLayoutAttributeTrailing)) {
CGFloat margin = constraint.constant;
CGPoint start = CGPointZero;
CGPoint end = CGPointMake(self.frame.size.width - (2 * margin), 0);
[self.horizontalSeparator addDottedLine:start to:end];
_isInitialized = YES;
break;
}
}

Resources