Asked  7 Months ago    Answers:  5   Viewed   157 times

Note: Things have moved on since this question was asked; see here for a good recent overview.


Before auto layout, you could change the anchor point of a view's layer without moving the view by storing the frame, setting the anchor point, and restoring the frame.

In an auto layout world, we don't set frames any more, but constraints don't seem up to the task of adjusting the position of a view back to where we want it to. You can hack the constraints to reposition your view, but on rotation or other resizing events, these become invalid again.

The following bright idea doesn't work as it creates an "Invalid pairing of layout attributes (left and width)":

layerView.layer.anchorPoint = CGPointMake(1.0, 0.5);
// Some other size-related constraints here which all work fine...
[self.view addConstraint:
    [NSLayoutConstraint constraintWithItem:layerView
                                 attribute:NSLayoutAttributeLeft
                                 relatedBy:NSLayoutRelationEqual 
                                    toItem:layerView 
                                 attribute:NSLayoutAttributeWidth 
                                multiplier:0.5 
                                  constant:20.0]];

My intention here was to set the left edge of layerView, the view with the adjusted anchor point, to half of its width plus 20 (the distance I want inset from the left edge of the superview).

Is it possible to change the anchor point, without changing the location of a view, in a view that is laid out with auto layout? Do I need to use hardcoded values and edit the constraint on every rotation? I do hope not.

I need to change the anchor point so that when I apply a transform to the view, I get the correct visual effect.

 Answers

70

[EDIT: Warning: The entire ensuing discussion will be possibly outmoded or at least heavily mitigated by iOS 8, which may no longer make the mistake of triggering layout at the time that a view transform is applied.]

Autolayout vs. View Transforms

Autolayout does not play at all well with view transforms. The reason, as far as I can discern, is that you're not supposed to mess with the frame of a view that has a transform (other than the default identity transform) - but that is exactly what autolayout does. The way autolayout works is that in layoutSubviews the runtime comes dashing through all the constraints and setting the frames of all the views accordingly.

In other words, the constraints are not magic; they are just a to-do list. layoutSubviews is where the to-do list gets done. And it does it by setting frames.

I can't help regarding this as a bug. If I apply this transform to a view:

v.transform = CGAffineTransformMakeScale(0.5,0.5);

I expect to see the view appear with its center in the same place as before and at half the size. But depending on its constraints, that may not be what I see at all.

[Actually, there's a second surprise here: applying a transform to a view triggers layout immediately. This seems to me be another bug. Or perhaps it's the heart of the first bug. What I would expect is to be able to get away with a transform at least until layout time, e.g. the device is rotated - just as I can get away with a frame animation until layout time. But in fact layout time is immediate, which seems just wrong.]

Solution 1: No Constraints

One current solution is, if I'm going to apply a semipermanent transform to a view (and not merely waggle it temporarily somehow), to remove all constraints affecting it. Unfortunately this typically causes the view to vanish from the screen, since autolayout still takes place, and now there are no constraints to tell us where to put the view. So in addition to removing the constraints, I set the view's translatesAutoresizingMaskIntoConstraints to YES. The view now works in the old way, effectively unaffected by autolayout. (It is affected by autolayout, obviously, but the implicit autoresizing mask constraints cause its behavior to be just like it was before autolayout.)

Solution 2: Use Only Appropriate Constraints

If that seems a bit drastic, another solution is to set the constraints to work correctly with an intended transform. If a view is sized purely by its internal fixed width and height, and positioned purely by its center, for example, my scale transform will work as I expect. In this code, I remove the existing constraints on a subview (otherView) and replace them with those four constraints, giving it a fixed width and height and pinning it purely by its center. After that, my scale transform works:

NSMutableArray* cons = [NSMutableArray array];
for (NSLayoutConstraint* con in self.view.constraints)
    if (con.firstItem == self.otherView || con.secondItem == self.otherView)
        [cons addObject:con];

[self.view removeConstraints:cons];
[self.otherView removeConstraints:self.otherView.constraints];
[self.view addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterX relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.otherView.center.x]];
[self.view addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:self.otherView.center.y]];
[self.otherView addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeWidth relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.width]];
[self.otherView addConstraint:
 [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.height]];

The upshot is that if you have no constraints that affect a view's frame, autolayout won't touch the view's frame - which is just what you're after when a transform is involved.

Solution 3: Use a Subview

The problem with both the above solutions is that we lose the benefits of constraints to position our view. So here's a solution that solves that. Start with an invisible view whose job is solely to act as a host, and use constraints to position it. Inside that, put the real view as a subview. Use constraints to position the subview within the host view, but limit those constraints to constraints that won't fight back when we apply a transform.

Here's an illustration:

enter image description here

The white view is host view; you are supposed to pretend that it is transparent and hence invisible. The red view is its subview, positioned by pinning its center to the host view's center. Now we can scale and rotate the red view around its center without any problem, and indeed the illustration shows that we have done so:

self.otherView.transform = CGAffineTransformScale(self.otherView.transform, 0.5, 0.5);
self.otherView.transform = CGAffineTransformRotate(self.otherView.transform, M_PI/8.0);

And meanwhile the constraints on the host view keep it in the right place as we rotate the device.

Solution 4: Use Layer Transforms Instead

Instead of view transforms, use layer transforms, which do not trigger layout and thus do not cause immediate conflict with constraints.

For example, this simple "throb" view animation may well break under autolayout:

[UIView animateWithDuration:0.3 delay:0
                    options:UIViewAnimationOptionAutoreverse
                 animations:^{
    v.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
    v.transform = CGAffineTransformIdentity;
}];

Even though in the end there was no change in the view's size, merely setting its transform causes layout to happen, and constraints can make the view jump around. (Does this feel like a bug or what?) But if we do the same thing with Core Animation (using a CABasicAnimation and applying the animation to the view's layer), layout doesn't happen, and it works fine:

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.autoreverses = YES;
ba.duration = 0.3;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1)];
[v.layer addAnimation:ba forKey:nil];
Tuesday, June 1, 2021
 
Shobit
answered 7 Months ago
31

You can write function_traits class as shown below, to discover the argument types, return type, and number of arguments:

template<typename T> 
struct function_traits;  

template<typename R, typename ...Args> 
struct function_traits<std::function<R(Args...)>>
{
    static const size_t nargs = sizeof...(Args);

    typedef R result_type;

    template <size_t i>
    struct arg
    {
        typedef typename std::tuple_element<i, std::tuple<Args...>>::type type;
    };
};

Test code:

struct R{};
struct A{};
struct B{};

int main()
{
   typedef std::function<R(A,B)> fun;

   std::cout << std::is_same<R, function_traits<fun>::result_type>::value << std::endl;
   std::cout << std::is_same<A, function_traits<fun>::arg<0>::type>::value << std::endl;
   std::cout << std::is_same<B, function_traits<fun>::arg<1>::type>::value << std::endl;
} 

Demo : http://ideone.com/YeN29

Wednesday, June 2, 2021
 
Tak
answered 7 Months ago
Tak
77

I had this problem as well.. It appears that the contentView's frame doesn't get updated until layoutSubviews is called however the frame of the cell is updated earlier leaving the contentView's frame set to {0, 0, 320, 44} at the time when the constraints are evaluated.

After looking at the contentView in more detail, It appears that autoresizingMasks are no longer being set.

Setting the autoresizingMask before you constrain your views can resolve this issue:

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
    if (self)
    {
        self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
        [self loadViews];
        [self constrainViews];
    }
    return self;
}
Wednesday, June 2, 2021
 
AntoineB
answered 7 Months ago
52

The number of constraints is not an issue. Both UILabel and UIButton will determine their size based on their intrinsicContentSize and since you have constraints for the position, it should have all the information it needs for layout.

However, when it comes to autolayout, UIScrollViews behave in a unique way. The best description comes from this technical note. There are two options available including examples, but heres a summary of each.


Mixed Approach

You just need to add a UIView to the UIScrollView then add and position all your subviews in the UIView. This requires you manually setting the frame of the UIView and the contentSize on the UIScrollView.

This is probably the easiest to use with the layout your trying to achieve. However, if the contentSize can change, you'll have to manually calculate and update the size.


Pure Auto Layout Approach

This option uses your autolayout constraints to determine the contentSize of the UIScrollView. This requires constraints going to all four edges of the UIScrollView and can not rely on the size of the UIScrollView.

This option is tougher to use since you need to make sure you have enough constraints. In your case, you'll run into issues because there are no constraints to the top and bottom of the UIScrollView and there are no constraints that can be used to determin the width of the UIScrollView. However, this option is amazing when you have to deal with dynamic content as it will resize the contentSize as needed.


Personally, I would go with the Pure Auto Layout Approach. It's ability to handle dynamic content sizes makes the extra constraint setup worth it.

If you post what you want the final layout to be, I'll update my answer to reflect that.


Update

Based on the images you posted, this is the way I would organize the subviews using the Pure Auto Layout Approach. The main difference is that the UIScrollView is now a a subview of the UIViewControllers view.

- UIView                                            (self.view)
  - UIScrollView                                    (scrollView)
    - UIView                                        (contentView)
      - UIImageView, UIButtons, UILabels, etc.

scrollView needs constraints so its edges are 0px from self.view.

contentView needs constraints so its edges are 0px from scrollView and that its width equals self.view. This is so the contentSize of the scrollView updates when you rotate the device.

Next just position all your images and labels the way you want. The label will need to be constrained to the left and right so it can calculate its height. The important thing to note is contentView will determine its height based on the constraints for its subviews, so you will need constraints "linking" the top and bottom of the contentView. A simeple example would look like this.

  • contentView top to imageView top
  • imageView height == some value
  • imageView bottom to label top
  • label bottom to contentView bottom
Tuesday, September 28, 2021
 
Uours
answered 2 Months ago
66

any views added as part of the tableViewCell subclass are positioned relative to the cells frame, ie x:0,y:0 for the subview origin, would be the top left corner of the tableCell. Something like this should be enough to get you started.

CGRect frame =[self frame];
frame.size.height=20.0f;
frame.origin.x=(self.frame.size.height/2)-frame.size.height;
frame.origin.y=0.0f;
[subview setFrame:frame];
Tuesday, October 26, 2021
 
dimi
answered 1 Month ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share