// // UIView+KeepLayout.m // Geografia // // Created by Martin Kiss on 21.10.12. // Copyright (c) 2012 iMartin Kiss. All rights reserved. // #import "UIView+KeepLayout.h" #import "KeepAttribute.h" #import <objc/runtime.h> @implementation UIView (KeepLayout) #pragma mark Associations - (KeepAttribute *)keep_attributeForSelector:(SEL)selector creationBlock:(KeepAttribute *(^)(void))creationBlock { NSParameterAssert(selector); KeepAttribute *attribute = objc_getAssociatedObject(self, selector); if ( ! attribute && creationBlock) { attribute = creationBlock(); objc_setAssociatedObject(self, selector, attribute, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return attribute; } - (KeepAttribute *)keep_attributeForSelector:(SEL)selector relatedView:(UIView *)relatedView creationBlock:(KeepAttribute *(^)())creationBlock { NSParameterAssert(selector); NSParameterAssert(relatedView); NSMapTable *attributesByRelatedView = objc_getAssociatedObject(self, selector); if ( ! attributesByRelatedView) { attributesByRelatedView = [NSMapTable weakToStrongObjectsMapTable]; objc_setAssociatedObject(self, selector, attributesByRelatedView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } KeepAttribute *attribute = [attributesByRelatedView objectForKey:relatedView]; if ( ! attribute && creationBlock) { attribute = creationBlock(); [attributesByRelatedView setObject:attribute forKey:relatedView]; } return attribute; } #pragma mark Dimensions - (KeepAttribute *)keep_dimensionForSelector:(SEL)selector dimensionAttribute:(NSLayoutAttribute)dimensionAttribute name:(NSString *)name { KeepParameterAssert(selector); KeepParameterAssert(dimensionAttribute == NSLayoutAttributeWidth || dimensionAttribute == NSLayoutAttributeHeight); KeepParameterAssert(name); return [self keep_attributeForSelector:selector creationBlock:^KeepAttribute *{ KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self layoutAttribute:dimensionAttribute relatedView:nil relatedLayoutAttribute:NSLayoutAttributeNotAnAttribute coefficient:1] name:@"%@ of <%@ %p>", name, self.class, self]; self.translatesAutoresizingMaskIntoConstraints = NO; return attribute; }]; } - (KeepAttribute *)keep_dimensionForSelector:(SEL)selector dimensionAttribute:(NSLayoutAttribute)dimensionAttribute relatedView:(UIView *)relatedView name:(NSString *)name { KeepParameterAssert(selector); KeepParameterAssert(dimensionAttribute == NSLayoutAttributeWidth || dimensionAttribute == NSLayoutAttributeHeight); KeepParameterAssert(relatedView); KeepParameterAssert(name); KeepAssert([self commonSuperview:relatedView], @"%@ requires both views to be in common hierarchy", NSStringFromSelector(selector)); return [self keep_attributeForSelector:selector relatedView:relatedView creationBlock:^KeepAttribute *{ KeepAttribute *attribute = [[KeepMultiplierAttribute alloc] initWithView:self layoutAttribute:dimensionAttribute relatedView:relatedView relatedLayoutAttribute:dimensionAttribute coefficient:1]; [attribute name:@"%@ of <%@ %p> to <%@ %p>", name, self.class, self, relatedView.class, relatedView]; self.translatesAutoresizingMaskIntoConstraints = NO; relatedView.translatesAutoresizingMaskIntoConstraints = NO; // Establish inverse relation [relatedView keep_attributeForSelector:_cmd relatedView:self creationBlock:^KeepAttribute *{ return attribute; }]; return attribute; }]; } - (KeepAttribute *)keepWidth { return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeWidth name:@"width"]; } - (KeepAttribute *)keepHeight { return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeHeight name:@"height"]; } - (KeepAttribute *)keepSize { return [[KeepAttribute group: self.keepWidth, self.keepHeight, nil] name:@"size of <%@ %p>", self.class, self]; } - (void)keepSize:(CGSize)size { [self keepSize:size priority:KeepPriorityRequired]; } - (void)keepSize:(CGSize)size priority:(KeepPriority)priority { self.keepWidth.equal = KeepValueMake(size.width, priority); self.keepHeight.equal = KeepValueMake(size.height, priority); } - (KeepAttribute *)keepAspectRatio { return [self keep_attributeForSelector:_cmd creationBlock:^KeepAttribute *{ KeepAttribute *attribute = [[KeepMultiplierAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeWidth relatedView:self relatedLayoutAttribute:NSLayoutAttributeHeight coefficient:1]; [attribute name:@"aspect ration of <%@ %p>", self.class, self]; self.translatesAutoresizingMaskIntoConstraints = NO; return attribute; }]; } - (KeepRelatedAttributeBlock)keepWidthTo { return ^KeepAttribute *(UIView *view) { return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeWidth relatedView:view name:@"width"]; }; } - (KeepRelatedAttributeBlock)keepHeightTo { return ^KeepAttribute *(UIView *view) { return [self keep_dimensionForSelector:_cmd dimensionAttribute:NSLayoutAttributeHeight relatedView:view name:@"height"]; }; } - (KeepRelatedAttributeBlock)keepSizeTo { return ^KeepAttribute *(UIView *view) { return [[KeepAttribute group: self.keepWidthTo(view), self.keepHeightTo(view), nil] name:@"size of <%@ %p> to <%@ %p>", self.class, self, view.class, view]; }; } #pragma mark Supreview Insets - (KeepAttribute *)keep_insetForSelector:(SEL)selector edgeAttribute:(NSLayoutAttribute)edgeAttribute coefficient:(CGFloat)coefficient name:(NSString *)name { KeepParameterAssert(selector); KeepParameterAssert(edgeAttribute == NSLayoutAttributeLeft || edgeAttribute == NSLayoutAttributeRight || edgeAttribute == NSLayoutAttributeTop || edgeAttribute == NSLayoutAttributeBottom || edgeAttribute == NSLayoutAttributeLeading || edgeAttribute == NSLayoutAttributeTrailing); KeepParameterAssert(name); KeepAssert(self.superview, @"Calling %@ allowed only when superview exists", NSStringFromSelector(selector)); return [self keep_attributeForSelector:selector creationBlock:^KeepAttribute *{ KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self layoutAttribute:edgeAttribute relatedView:self.superview relatedLayoutAttribute:edgeAttribute coefficient:coefficient] name:@"%@ of <%@ %p> to superview <%@ %p>", name, self.class, self, self.superview.class, self.superview]; self.translatesAutoresizingMaskIntoConstraints = NO; return attribute; }]; } - (KeepAttribute *)keepLeftInset { return [self keep_insetForSelector:_cmd edgeAttribute:NSLayoutAttributeLeft coefficient:1 name:@"left inset"]; } - (KeepAttribute *)keepRightInset { return [self keep_insetForSelector:_cmd edgeAttribute:NSLayoutAttributeRight coefficient:-1 name:@"right inset"]; } - (KeepAttribute *)keepTopInset { return [self keep_insetForSelector:_cmd edgeAttribute:NSLayoutAttributeTop coefficient:1 name:@"top inset"]; } - (KeepAttribute *)keepBottomInset { return [self keep_insetForSelector:_cmd edgeAttribute:NSLayoutAttributeBottom coefficient:-1 name:@"bottom inset"]; } - (KeepAttribute *)keepInsets { return [[[KeepGroupAttribute alloc] initWithAttributes:@[ self.keepTopInset, self.keepBottomInset, self.keepLeftInset, self.keepRightInset ]] name:@"all insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; } - (KeepAttribute *)keepHorizontalInsets { return [[[KeepGroupAttribute alloc] initWithAttributes:@[ self.keepLeftInset, self.keepRightInset ]] name:@"horizontal insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; } - (KeepAttribute *)keepVerticalInsets { return [[[KeepGroupAttribute alloc] initWithAttributes:@[ self.keepTopInset, self.keepBottomInset ]] name:@"vertical insets of <%@ %p> to superview <%@ %p>", self.class, self, self.superview.class, self.superview]; } - (void)keepInsets:(UIEdgeInsets)insets { [self keepInsets:insets priority:KeepPriorityRequired]; } - (void)keepInsets:(UIEdgeInsets)insets priority:(KeepPriority)priority { self.keepLeftInset.equal = KeepValueMake(insets.left, priority); self.keepRightInset.equal = KeepValueMake(insets.right, priority); self.keepTopInset.equal = KeepValueMake(insets.top, priority); self.keepBottomInset.equal = KeepValueMake(insets.bottom, priority); } #pragma mark Center - (KeepAttribute *)keep_centerForSelector:(SEL)selector centerAttribute:(NSLayoutAttribute)centerAttribute name:(NSString *)name { KeepParameterAssert(selector); KeepParameterAssert(centerAttribute == NSLayoutAttributeCenterX || centerAttribute == NSLayoutAttributeCenterY); KeepParameterAssert(name); KeepAssert(self.superview, @"Calling %@ allowed only when superview exists", NSStringFromSelector(selector)); return [self keep_attributeForSelector:selector creationBlock:^KeepAttribute *{ KeepAttribute *attribute = [[[KeepMultiplierAttribute alloc] initWithView:self layoutAttribute:centerAttribute relatedView:self.superview relatedLayoutAttribute:centerAttribute coefficient:2] name:@"%@ of <%@ %p> in superview <%@ %p>", name, self.class, self, self.superview.class, self.superview]; self.translatesAutoresizingMaskIntoConstraints = NO; return attribute; }]; } - (KeepAttribute *)keepHorizontalCenter { return [self keep_centerForSelector:_cmd centerAttribute:NSLayoutAttributeCenterX name:@"horizontal center"]; } - (KeepAttribute *)keepVerticalCenter { return [self keep_centerForSelector:_cmd centerAttribute:NSLayoutAttributeCenterY name:@"vertical center"]; } - (KeepAttribute *)keepCenter { return [[[KeepGroupAttribute alloc] initWithAttributes:@[ self.keepHorizontalCenter, self.keepVerticalCenter ]] name:@"center of <%@ %p> in superview <%@ %p>", self.class, self, self.superview.class, self.superview]; } - (void)keepCentered { [self keepCenteredWithPriority:KeepPriorityRequired]; } - (void)keepCenteredWithPriority:(KeepPriority)priority { [self keepCenter:CGPointMake(0.5, 0.5) priority:priority]; } - (void)keepHorizontallyCentered { [self keepHorizontallyCenteredWithPriority:KeepPriorityRequired]; } - (void)keepHorizontallyCenteredWithPriority:(KeepPriority)priority { self.keepHorizontalCenter.equal = KeepValueMake(0.5, priority); } - (void)keepVerticallyCentered { [self keepVerticallyCenteredWithPriority:KeepPriorityRequired]; } - (void)keepVerticallyCenteredWithPriority:(KeepPriority)priority { self.keepVerticalCenter.equal = KeepValueMake(0.5, priority); } - (void)keepCenter:(CGPoint)center { [self keepCenter:center priority:KeepPriorityRequired]; } - (void)keepCenter:(CGPoint)center priority:(KeepPriority)priority { self.keepHorizontalCenter.equal = KeepValueMake(center.x, priority); self.keepVerticalCenter.equal = KeepValueMake(center.y, priority); } #pragma mark Offsets - (KeepAttribute *)keep_offsetForSelector:(SEL)selector edgeAttribute:(NSLayoutAttribute)edgeAttribute relatedView:(UIView *)relatedView name:(NSString *)name { KeepParameterAssert(selector); KeepParameterAssert(edgeAttribute == NSLayoutAttributeLeft || edgeAttribute == NSLayoutAttributeRight || edgeAttribute == NSLayoutAttributeTop || edgeAttribute == NSLayoutAttributeBottom || edgeAttribute == NSLayoutAttributeLeading || edgeAttribute == NSLayoutAttributeTrailing); KeepParameterAssert(relatedView); KeepParameterAssert(name); KeepAssert([self commonSuperview:relatedView], @"%@ requires both views to be in common hierarchy", NSStringFromSelector(selector)); return [self keep_attributeForSelector:selector relatedView:relatedView creationBlock:^KeepAttribute *{ NSDictionary *oppositeEdges = @{ @(NSLayoutAttributeLeft): @(NSLayoutAttributeRight), @(NSLayoutAttributeRight): @(NSLayoutAttributeLeft), @(NSLayoutAttributeTop): @(NSLayoutAttributeBottom), @(NSLayoutAttributeBottom): @(NSLayoutAttributeTop), }; KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self layoutAttribute:edgeAttribute relatedView:relatedView relatedLayoutAttribute:[[oppositeEdges objectForKey:@(edgeAttribute)] integerValue] coefficient:1] name:@"%@ of <%@ %p> to <%@ %p>", name, self.class, self, relatedView.class, relatedView]; self.translatesAutoresizingMaskIntoConstraints = NO; relatedView.translatesAutoresizingMaskIntoConstraints = NO; return attribute; }]; } - (KeepRelatedAttributeBlock)keepLeftOffsetTo { return ^KeepAttribute *(UIView *view) { return [self keep_offsetForSelector:_cmd edgeAttribute:NSLayoutAttributeLeft relatedView:view name:@"left offset"]; }; } - (KeepRelatedAttributeBlock)keepRightOffsetTo { return ^KeepAttribute *(UIView *view) { return view.keepLeftOffsetTo(self); }; } - (KeepRelatedAttributeBlock)keepTopOffsetTo { return ^KeepAttribute *(UIView *view) { return [self keep_offsetForSelector:_cmd edgeAttribute:NSLayoutAttributeTop relatedView:view name:@"top offset"]; }; } - (KeepRelatedAttributeBlock)keepBottomOffsetTo { return ^KeepAttribute *(UIView *view) { return view.keepTopOffsetTo(self); }; } #pragma mark Alignments - (KeepAttribute *)keep_alignForSelector:(SEL)selector alignAttribute:(NSLayoutAttribute)alignAttribute relatedView:(UIView *)relatedView coefficient:(CGFloat)coefficient name:(NSString *)name { KeepParameterAssert(selector); KeepParameterAssert(alignAttribute == NSLayoutAttributeLeft || alignAttribute == NSLayoutAttributeRight || alignAttribute == NSLayoutAttributeTop || alignAttribute == NSLayoutAttributeBottom || alignAttribute == NSLayoutAttributeLeading || alignAttribute == NSLayoutAttributeTrailing || alignAttribute == NSLayoutAttributeCenterX || alignAttribute == NSLayoutAttributeBaseline || alignAttribute == NSLayoutAttributeCenterY); KeepParameterAssert(relatedView); KeepParameterAssert(name); KeepAssert([self commonSuperview:relatedView], @"%@ requires both views to be in common hierarchy", NSStringFromSelector(selector)); return [self keep_attributeForSelector:selector relatedView:relatedView creationBlock:^KeepAttribute *{ KeepAttribute *attribute = [[[KeepConstantAttribute alloc] initWithView:self layoutAttribute:alignAttribute relatedView:relatedView relatedLayoutAttribute:alignAttribute coefficient:coefficient] name:@"%@ of <%@ %p> to <%@ %p>", name, self.class, self, relatedView.class, relatedView]; self.translatesAutoresizingMaskIntoConstraints = NO; relatedView.translatesAutoresizingMaskIntoConstraints = NO; // Establish inverse attribute [relatedView keep_attributeForSelector:selector relatedView:self creationBlock:^KeepAttribute *{ return attribute; }]; return attribute; }]; } - (KeepRelatedAttributeBlock)keepLeftAlignTo { return ^KeepAttribute *(UIView *view) { return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeLeft relatedView:view coefficient:1 name:@"left alignment"]; }; } - (KeepRelatedAttributeBlock)keepRightAlignTo { return ^KeepAttribute *(UIView *view) { return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeRight relatedView:view coefficient:-1 name:@"right alignment"]; }; } - (KeepRelatedAttributeBlock)keepTopAlignTo { return ^KeepAttribute *(UIView *view) { return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeTop relatedView:view coefficient:1 name:@"top alignment"]; }; } - (KeepRelatedAttributeBlock)keepBottomAlignTo { return ^KeepAttribute *(UIView *view) { return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeBottom relatedView:view coefficient:-1 name:@"bottom alignment"]; }; } - (void)keepEdgeAlignTo:(UIView *)view { [self keepEdgeAlignTo:view insets:UIEdgeInsetsZero]; } - (void)keepEdgeAlignTo:(UIView *)view insets:(UIEdgeInsets)insets { [self keepEdgeAlignTo:view insets:insets withPriority:KeepPriorityRequired]; } - (void)keepEdgeAlignTo:(UIView *)view insets:(UIEdgeInsets)insets withPriority:(KeepPriority)priority { self.keepLeftAlignTo(view).equal = KeepValueMake(insets.left, priority); self.keepRightAlignTo(view).equal = KeepValueMake(insets.right, priority); self.keepTopAlignTo(view).equal = KeepValueMake(insets.top, priority); self.keepBottomAlignTo(view).equal = KeepValueMake(insets.bottom, priority); } - (KeepRelatedAttributeBlock)keepVerticalAlignTo { return ^KeepAttribute *(UIView *view) { return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeCenterX relatedView:view coefficient:1 name:@"vertical center alignment"]; }; } - (KeepRelatedAttributeBlock)keepHorizontalAlignTo { return ^KeepAttribute *(UIView *view) { return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeCenterY relatedView:view coefficient:1 name:@"horizontal center alignment"]; }; } - (void)keepCenterAlignTo:(UIView *)view { [self keepCenterAlignTo:view offset:UIOffsetZero]; } - (void)keepCenterAlignTo:(UIView *)view offset:(UIOffset)offset { [self keepCenterAlignTo:view offset:offset withPriority:KeepPriorityRequired]; } - (void)keepCenterAlignTo:(UIView *)view offset:(UIOffset)offset withPriority:(KeepPriority)priority { self.keepHorizontalAlignTo(view).equal = KeepValueMake(offset.horizontal, priority); self.keepVerticalAlignTo(view).equal = KeepValueMake(offset.vertical, priority); } - (KeepRelatedAttributeBlock)keepBaselineAlignTo { return ^KeepAttribute *(UIView *view) { return [self keep_alignForSelector:_cmd alignAttribute:NSLayoutAttributeBaseline relatedView:view coefficient:-1 name:@"baseline alignment"]; }; } #pragma mark Animating Constraints - (void)keepAnimatedWithDuration:(NSTimeInterval)duration layout:(void(^)(void))animations { KeepParameterAssert(duration >= 0); KeepParameterAssert(animations); [self keep_animationPerformWithDuration:duration delay:0 block:^{ [UIView animateWithDuration:duration animations:^{ animations(); [self layoutIfNeeded]; }]; }]; } - (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay layout:(void(^)(void))animations { KeepParameterAssert(duration >= 0); KeepParameterAssert(delay >= 0); KeepParameterAssert(animations); [self keep_animationPerformWithDuration:duration delay:delay block:^{ [UIView animateWithDuration:duration animations:^{ animations(); [self layoutIfNeeded]; }]; }]; } - (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options layout:(void(^)(void))animations completion:(void(^)(BOOL finished))completion { KeepParameterAssert(duration >= 0); KeepParameterAssert(delay >= 0); KeepParameterAssert(animations); [self keep_animationPerformWithDuration:duration delay:delay block:^{ [UIView animateWithDuration:duration delay:0 options:options animations:^{ animations(); [self layoutIfNeeded]; } completion:completion]; }]; } #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 // Compiled with iOS 7 SDK - (void)keepAnimatedWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options layout:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { KeepParameterAssert(duration >= 0); KeepParameterAssert(delay >= 0); KeepParameterAssert(animations); if ([UIView respondsToSelector:@selector(animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)]) { // Running on iOS 7 [self keep_animationPerformWithDuration:duration delay:delay block:^{ [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:dampingRatio initialSpringVelocity:velocity options:options animations:^{ animations(); [self layoutIfNeeded]; } completion:completion]; }]; } else { // Running on iOS 6, fallback to non-spring animation [self keep_animationPerformWithDuration:duration delay:delay block:^{ [UIView animateWithDuration:duration delay:0 options:options animations:^{ animations(); [self layoutIfNeeded]; } completion:completion]; }]; } } #endif - (void)keep_animationPerformWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay block:(void(^)(void))block { if (duration > 0 || delay > 0) { [[NSOperationQueue mainQueue] performSelector:@selector(addOperationWithBlock:) withObject:block afterDelay:delay inModes:@[NSRunLoopCommonModes]]; } else { block(); } } #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 // Compiled with iOS 7 SDK - (void)keepNotAnimated:(void (^)(void))layout { if ([UIView respondsToSelector:@selector(performWithoutAnimation:)]) { // Running iOS 7 [UIView performWithoutAnimation:^{ layout(); [self layoutIfNeeded]; }]; } else { // Running iOS 6, just execute block layout(); [self layoutIfNeeded]; } } #endif #pragma mark Common Superview - (UIView *)commonSuperview:(UIView *)anotherView { UIView *superview = self; while (superview) { if ([anotherView isDescendantOfView:superview]) { break; // The `superview` is common for both views. } superview = superview.superview; } return superview; } #pragma mark Convenience Auto Layout - (void)addConstraintToCommonSuperview:(NSLayoutConstraint *)constraint { UIView *relatedLayoutView = constraint.secondItem; UIView *commonView = (relatedLayoutView? [self commonSuperview:relatedLayoutView] : self); [commonView addConstraint:constraint]; } - (void)removeConstraintFromCommonSuperview:(NSLayoutConstraint *)constraint { UIView *relatedLayoutView = constraint.secondItem; UIView *commonView = (relatedLayoutView? [self commonSuperview:relatedLayoutView] : self); [commonView removeConstraint:constraint]; } - (void)addConstraintsToCommonSuperview:(id<NSFastEnumeration>)constraints { for (NSLayoutConstraint *constraint in constraints) { [self addConstraintToCommonSuperview:constraint]; } } - (void)removeConstraintsFromCommonSuperview:(id<NSFastEnumeration>)constraints { for (NSLayoutConstraint *constraint in constraints) { [self removeConstraintFromCommonSuperview:constraint]; } } @end