From c940a567b01002480cc6bcb7a89e4759cd806842 Mon Sep 17 00:00:00 2001 From: Jonas Budelmann Date: Fri, 25 Oct 2013 08:52:52 +1100 Subject: [PATCH 1/4] Update constraint. If constraint that matches all properties apart from constant is found --- Masonry/MASViewConstraint.m | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Masonry/MASViewConstraint.m b/Masonry/MASViewConstraint.m index 4dc12c68..0d77d5af 100644 --- a/Masonry/MASViewConstraint.m +++ b/Masonry/MASViewConstraint.m @@ -291,15 +291,40 @@ - (void)install { @"couldn't find a common superview for %@ and %@", firstLayoutItem, secondLayoutItem); self.installedView = closestCommonSuperview; - [closestCommonSuperview addConstraint:layoutConstraint]; - self.layoutConstraint = layoutConstraint; } else { self.installedView = firstLayoutItem; - [firstLayoutItem addConstraint:layoutConstraint]; + } + + MASLayoutConstraint *existingConstraint = [self layoutConstraintSimiliarTo:layoutConstraint]; + if (existingConstraint) { + // just update the constant + existingConstraint.constant = layoutConstraint.constant; + self.layoutConstraint = existingConstraint; + } else { + [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; } } +- (MASLayoutConstraint *)layoutConstraintSimiliarTo:(MASLayoutConstraint *)layoutConstraint { + // check if any constraints are the same apart from the only mutable property constant + + // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints + // and they are likely to be added first. + for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) { + if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue; + if (existingConstraint.firstItem != layoutConstraint.firstItem) continue; + if (existingConstraint.secondItem != layoutConstraint.secondItem) continue; + if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue; + if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue; + if (existingConstraint.multiplier != layoutConstraint.multiplier) continue; + if (existingConstraint.priority != layoutConstraint.priority) continue; + + return (id)existingConstraint; + } + return nil; +} + - (void)uninstall { [self.installedView removeConstraint:self.layoutConstraint]; self.layoutConstraint = nil; From fa01db45872f8d8c0f67b07c3f46222427fb4d09 Mon Sep 17 00:00:00 2001 From: Jonas Budelmann Date: Sun, 3 Nov 2013 18:09:35 +1100 Subject: [PATCH 2/4] adding new mas_updateConstraints method --- Masonry/MASCompositeConstraint.m | 2 ++ Masonry/MASConstraint.h | 5 +++++ Masonry/MASConstraintMaker.h | 5 +++++ Masonry/MASConstraintMaker.m | 1 + Masonry/MASViewConstraint.m | 7 ++++++- Masonry/View+MASAdditions.h | 11 +++++++++++ Masonry/View+MASAdditions.m | 8 ++++++++ Masonry/View+MASShorthandAdditions.h | 5 +++++ 8 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Masonry/MASCompositeConstraint.m b/Masonry/MASCompositeConstraint.m index a41b7b7d..e8725a75 100644 --- a/Masonry/MASCompositeConstraint.m +++ b/Masonry/MASCompositeConstraint.m @@ -18,6 +18,7 @@ @interface MASCompositeConstraint () @implementation MASCompositeConstraint @synthesize delegate = _delegate; +@synthesize updateExisting = _updateExisting; - (id)initWithChildren:(NSArray *)children { self = [super init]; @@ -181,6 +182,7 @@ - (void)constraint:(id)constraint shouldBeReplacedWithConstraint: - (void)install { for (id constraint in self.childConstraints) { + constraint.updateExisting = self.updateExisting; [constraint install]; } } diff --git a/Masonry/MASConstraint.h b/Masonry/MASConstraint.h index dcc2ea0f..c08625b4 100644 --- a/Masonry/MASConstraint.h +++ b/Masonry/MASConstraint.h @@ -113,6 +113,11 @@ */ @property (nonatomic, copy, readonly) id (^key)(id key); +/** + * Whether or not to check for an existing constraint instead of adding constraint + */ +@property (nonatomic, assign) BOOL updateExisting; + /** * Creates a NSLayoutConstraint. The constraint is installed to the first view or the or the closest common superview of the first and second view. */ diff --git a/Masonry/MASConstraintMaker.h b/Masonry/MASConstraintMaker.h index 1a106449..75a95ae1 100644 --- a/Masonry/MASConstraintMaker.h +++ b/Masonry/MASConstraintMaker.h @@ -53,6 +53,11 @@ */ @property (nonatomic, strong, readonly) id center; +/** + * Whether or not to check for an existing constraint instead of adding constraint + */ +@property (nonatomic, assign) BOOL updateExisting; + /** * initialises the maker with a default view * diff --git a/Masonry/MASConstraintMaker.m b/Masonry/MASConstraintMaker.m index c8d929b2..2ced0b28 100644 --- a/Masonry/MASConstraintMaker.m +++ b/Masonry/MASConstraintMaker.m @@ -34,6 +34,7 @@ - (id)initWithView:(MAS_VIEW *)view { - (NSArray *)install { NSArray *constraints = self.constraints.copy; for (id constraint in constraints) { + constraint.updateExisting = self.updateExisting; [constraint install]; } [self.constraints removeAllObjects]; diff --git a/Masonry/MASViewConstraint.m b/Masonry/MASViewConstraint.m index 0d77d5af..e2b8eb54 100644 --- a/Masonry/MASViewConstraint.m +++ b/Masonry/MASViewConstraint.m @@ -28,6 +28,7 @@ @interface MASViewConstraint () @implementation MASViewConstraint @synthesize delegate = _delegate; +@synthesize updateExisting = _updateExisting; - (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute { self = [super init]; @@ -295,7 +296,11 @@ - (void)install { self.installedView = firstLayoutItem; } - MASLayoutConstraint *existingConstraint = [self layoutConstraintSimiliarTo:layoutConstraint]; + + MASLayoutConstraint *existingConstraint = nil; + if (self.updateExisting) { + existingConstraint = [self layoutConstraintSimiliarTo:layoutConstraint]; + } if (existingConstraint) { // just update the constant existingConstraint.constant = layoutConstraint.constant; diff --git a/Masonry/View+MASAdditions.h b/Masonry/View+MASAdditions.h index 64c9981a..90ceb5e3 100644 --- a/Masonry/View+MASAdditions.h +++ b/Masonry/View+MASAdditions.h @@ -55,4 +55,15 @@ */ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block; +/** + * Creates a MASConstraintMaker with the callee view. + * Any constraints defined are added to the view or the appropriate superview once the block has finished executing. + * If an existing constraint exists then it will be updated instead. + * + * @param block scope within which you can build up the constraints which you wish to apply to the view. + * + * @return Array of created/updated MASConstraints + */ +- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block; + @end diff --git a/Masonry/View+MASAdditions.m b/Masonry/View+MASAdditions.m index 7b285c11..201c3586 100644 --- a/Masonry/View+MASAdditions.m +++ b/Masonry/View+MASAdditions.m @@ -18,6 +18,14 @@ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { return [constraintMaker install]; } +- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block { + self.translatesAutoresizingMaskIntoConstraints = NO; + MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; + block(constraintMaker); + constraintMaker.updateExisting = YES; + return [constraintMaker install]; +} + #pragma mark - NSLayoutAttribute properties - (MASViewAttribute *)mas_left { diff --git a/Masonry/View+MASShorthandAdditions.h b/Masonry/View+MASShorthandAdditions.h index b70f0f45..c527a2f1 100644 --- a/Masonry/View+MASShorthandAdditions.h +++ b/Masonry/View+MASShorthandAdditions.h @@ -29,6 +29,7 @@ @property (nonatomic, strong, readonly) MASViewAttribute *baseline; - (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *make))block; +- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *make))block; @end @@ -55,6 +56,10 @@ MAS_ATTR_FORWARD(baseline); return [self mas_makeConstraints:block]; } +- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *))block { + return [self mas_updateConstraints:block]; +} + @end #endif From cbe111d90ed3132a88787be444b384425534b1ee Mon Sep 17 00:00:00 2001 From: Jonas Budelmann Date: Sun, 3 Nov 2013 18:29:01 +1100 Subject: [PATCH 3/4] update existing constraint tests --- MasonryTests/MASConstraintMakerSpec.m | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/MasonryTests/MASConstraintMakerSpec.m b/MasonryTests/MASConstraintMakerSpec.m index 2c037f5f..d93a1400 100644 --- a/MasonryTests/MASConstraintMakerSpec.m +++ b/MasonryTests/MASConstraintMakerSpec.m @@ -104,6 +104,47 @@ @interface MASCompositeConstraint () expect([maker install]).to.haveCountOf(2); }); +it(@"should update constraints", ^{ + MAS_VIEW *newView = MAS_VIEW.new; + [superview addSubview:newView]; + + maker.updateExisting = YES; + maker.left.equalTo(newView).offset(10); + [maker install]; + + NSLayoutConstraint *constraint1 = superview.constraints[0]; + expect(constraint1.constant).to.equal(10); + + maker.left.equalTo(newView).offset(20); + [maker install]; + + expect(superview.constraints).to.haveCountOf(1); + NSLayoutConstraint *constraint2 = superview.constraints[0]; + expect(constraint2.constant).to.equal(20); + + expect(constraint2).to.beIdenticalTo(constraint2); +}); + +it(@"should not update constraint", ^{ + MAS_VIEW *newView = MAS_VIEW.new; + [superview addSubview:newView]; + + maker.updateExisting = YES; + maker.left.equalTo(newView).offset(10); + [maker install]; + + NSLayoutConstraint *constraint1 = superview.constraints[0]; + expect(constraint1.constant).to.equal(10); + + maker.right.equalTo(newView).offset(20); + [maker install]; + + expect(superview.constraints).to.haveCountOf(2); + NSLayoutConstraint *constraint2 = superview.constraints[1]; + expect(constraint1.constant).to.equal(10); + expect(constraint2.constant).to.equal(20); +}); + it(@"should create new constraints", ^{ expect(maker.left).notTo.beIdenticalTo(maker.left); expect(maker.right).notTo.beIdenticalTo(maker.right); From 19a7e72103eafb322964d7889311d56380b4d1e4 Mon Sep 17 00:00:00 2001 From: Jonas Budelmann Date: Sun, 3 Nov 2013 19:16:02 +1100 Subject: [PATCH 4/4] adding mas_updateConstraints example --- Masonry/View+MASAdditions.m | 2 +- .../project.pbxproj | 6 ++ .../MASExampleListViewController.m | 3 + .../MASExampleUpdateView.h | 13 ++++ .../MASExampleUpdateView.m | 59 +++++++++++++++++++ MasonryTests/MASViewAttributeSpec.m | 4 ++ ...NSLayoutConstraint+MASDebugAdditionsSpec.m | 4 +- MasonryTests/View+MASAdditionsSpec.m | 9 +++ 8 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.h create mode 100644 MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.m diff --git a/Masonry/View+MASAdditions.m b/Masonry/View+MASAdditions.m index 201c3586..fc5b0fde 100644 --- a/Masonry/View+MASAdditions.m +++ b/Masonry/View+MASAdditions.m @@ -21,8 +21,8 @@ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; - block(constraintMaker); constraintMaker.updateExisting = YES; + block(constraintMaker); return [constraintMaker install]; } diff --git a/MasonryExamples/Masonry iOS Examples.xcodeproj/project.pbxproj b/MasonryExamples/Masonry iOS Examples.xcodeproj/project.pbxproj index 38125295..9e5b9aa0 100644 --- a/MasonryExamples/Masonry iOS Examples.xcodeproj/project.pbxproj +++ b/MasonryExamples/Masonry iOS Examples.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + DD175E6A182639FB0099129A /* MASExampleUpdateView.m in Sources */ = {isa = PBXBuildFile; fileRef = DD175E69182639FB0099129A /* MASExampleUpdateView.m */; }; DD52F22B179CAD57005CD195 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD52F22A179CAD57005CD195 /* UIKit.framework */; }; DD52F22D179CAD57005CD195 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD52F22C179CAD57005CD195 /* Foundation.framework */; }; DD52F22F179CAD57005CD195 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD52F22E179CAD57005CD195 /* CoreGraphics.framework */; }; @@ -31,6 +32,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + DD175E68182639FB0099129A /* MASExampleUpdateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASExampleUpdateView.h; sourceTree = ""; }; + DD175E69182639FB0099129A /* MASExampleUpdateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASExampleUpdateView.m; sourceTree = ""; }; DD52F227179CAD57005CD195 /* Masonry iOS Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Masonry iOS Examples.app"; sourceTree = BUILT_PRODUCTS_DIR; }; DD52F22A179CAD57005CD195 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; DD52F22C179CAD57005CD195 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; @@ -157,6 +160,8 @@ DD7CC17517ACE990007A469E /* MASExampleDebuggingView.m */, DDDF60CA181915E300BF7B8B /* MASExampleLabelView.h */, DDDF60CB181915E300BF7B8B /* MASExampleLabelView.m */, + DD175E68182639FB0099129A /* MASExampleUpdateView.h */, + DD175E69182639FB0099129A /* MASExampleUpdateView.m */, ); name = Views; sourceTree = ""; @@ -261,6 +266,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DD175E6A182639FB0099129A /* MASExampleUpdateView.m in Sources */, DD52F237179CAD57005CD195 /* main.m in Sources */, DD52F23B179CAD57005CD195 /* MASAppDelegate.m in Sources */, DD52F251179CADC0005CD195 /* MASExampleBasicView.m in Sources */, diff --git a/MasonryExamples/Masonry iOS Examples/MASExampleListViewController.m b/MasonryExamples/Masonry iOS Examples/MASExampleListViewController.m index 3a21a231..6ae625af 100644 --- a/MasonryExamples/Masonry iOS Examples/MASExampleListViewController.m +++ b/MasonryExamples/Masonry iOS Examples/MASExampleListViewController.m @@ -14,6 +14,7 @@ #import "MASExampleAnimatedView.h" #import "MASExampleDebuggingView.h" #import "MASExampleLabelView.h" +#import "MASExampleUpdateView.h" static NSString * const kMASCellReuseIdentifier = @"kMASCellReuseIdentifier"; @@ -44,6 +45,8 @@ - (id)init { viewClass:MASExampleDebuggingView.class], [[MASExampleViewController alloc] initWithTitle:@"Bacony Labels" viewClass:MASExampleLabelView.class], + [[MASExampleViewController alloc] initWithTitle:@"Update constraints" + viewClass:MASExampleUpdateView.class], ]; return self; diff --git a/MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.h b/MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.h new file mode 100644 index 00000000..14cb79e0 --- /dev/null +++ b/MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.h @@ -0,0 +1,13 @@ +// +// MASExampleUpdateView.h +// Masonry iOS Examples +// +// Created by Jonas Budelmann on 3/11/13. +// Copyright (c) 2013 Jonas Budelmann. All rights reserved. +// + +#import + +@interface MASExampleUpdateView : UIView + +@end diff --git a/MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.m b/MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.m new file mode 100644 index 00000000..343e2e74 --- /dev/null +++ b/MasonryExamples/Masonry iOS Examples/MASExampleUpdateView.m @@ -0,0 +1,59 @@ +// +// MASExampleUpdateView.m +// Masonry iOS Examples +// +// Created by Jonas Budelmann on 3/11/13. +// Copyright (c) 2013 Jonas Budelmann. All rights reserved. +// + +#import "MASExampleUpdateView.h" + +@interface MASExampleUpdateView () + +@property (nonatomic, strong) UIButton *growingButton; +@property (nonatomic, assign) CGSize buttonSize; + +@end + +@implementation MASExampleUpdateView + +- (id)init { + self = [super init]; + if (!self) return nil; + + self.growingButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.growingButton setTitle:@"Grow Me!" forState:UIControlStateNormal]; + self.growingButton.layer.borderColor = UIColor.greenColor.CGColor; + self.growingButton.layer.borderWidth = 2; + + [self.growingButton addTarget:self action:@selector(didTapGrowButton:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:self.growingButton]; + + self.buttonSize = CGSizeMake(100, 100); + [self setNeedsUpdateConstraints]; + + return self; +} + +- (void)updateConstraints { + [super updateConstraints]; + + [self.growingButton updateConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self); + make.width.equalTo(@(self.buttonSize.width)); + make.height.equalTo(@(self.buttonSize.height)); + }]; +} + +- (void)didTapGrowButton:(UIButton *)button { + self.buttonSize = CGSizeMake(self.buttonSize.width * 1.1, self.buttonSize.height * 1.1); + + [self setNeedsUpdateConstraints]; + [self updateConstraintsIfNeeded]; + + [UIView animateWithDuration:0.4 animations:^{ + [self layoutIfNeeded]; + }]; +} + +@end diff --git a/MasonryTests/MASViewAttributeSpec.m b/MasonryTests/MASViewAttributeSpec.m index 32bf9bb9..d476dedb 100644 --- a/MasonryTests/MASViewAttributeSpec.m +++ b/MasonryTests/MASViewAttributeSpec.m @@ -45,6 +45,10 @@ layoutAttribute:NSLayoutAttributeRight]; expect([viewAttribute isEqual:otherViewAttribute]).to.equal(NO); }); + + it(@"should return NO when non view attribute passed", ^{ + expect([viewAttribute isEqual:NSArray.new]).to.equal(NO); + }); }); context(@"hash", ^{ diff --git a/MasonryTests/NSLayoutConstraint+MASDebugAdditionsSpec.m b/MasonryTests/NSLayoutConstraint+MASDebugAdditionsSpec.m index d8b004d0..5888a407 100644 --- a/MasonryTests/NSLayoutConstraint+MASDebugAdditionsSpec.m +++ b/MasonryTests/NSLayoutConstraint+MASDebugAdditionsSpec.m @@ -32,10 +32,10 @@ MAS_VIEW *newView2 = MAS_VIEW.new; newView2.mas_key = @"newView2"; - MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:newView1 attribute:NSLayoutAttributeBaseline relatedBy:NSLayoutRelationEqual toItem:newView2 attribute:NSLayoutAttributeTop multiplier:1 constant:300]; + MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:newView1 attribute:NSLayoutAttributeBaseline relatedBy:NSLayoutRelationEqual toItem:newView2 attribute:NSLayoutAttributeTop multiplier:2 constant:300]; layoutConstraint.mas_key = @"helloConstraint"; - NSString *description = [NSString stringWithFormat:@"", MAS_VIEW.class, MAS_VIEW.class]; + NSString *description = [NSString stringWithFormat:@"", MAS_VIEW.class, MAS_VIEW.class]; expect([layoutConstraint description]).to.equal(description); }); diff --git a/MasonryTests/View+MASAdditionsSpec.m b/MasonryTests/View+MASAdditionsSpec.m index ead79bee..6fb6d393 100644 --- a/MasonryTests/View+MASAdditionsSpec.m +++ b/MasonryTests/View+MASAdditionsSpec.m @@ -13,7 +13,16 @@ it(@"should set translatesAutoresizingMaskIntoConstraints", ^{ MAS_VIEW *newView = MAS_VIEW.new; [newView mas_makeConstraints:^(MASConstraintMaker *make) { + expect(make.updateExisting).to.beFalsy(); + }]; + expect(newView.translatesAutoresizingMaskIntoConstraints).to.beFalsy(); +}); + +it(@"should set updateExisting", ^{ + MAS_VIEW *newView = MAS_VIEW.new; + [newView mas_updateConstraints:^(MASConstraintMaker *make) { + expect(make.updateExisting).to.beTruthy(); }]; expect(newView.translatesAutoresizingMaskIntoConstraints).to.beFalsy();