diff --git a/.travis.yml b/.travis.yml index bd536c9..74549d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ branches: - develop before_install: - bundle install - - instruments -s script: - set -o pipefail - fastlane travis diff --git a/CHANGELOG.md b/CHANGELOG.md index 36eccdb..f075c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v.1.1.1 + +* Grouped deactivation of `NSLayoutConstraints`. Before, the deactivation of `NSLayoutConstraints` was taking place upon finding +a conflict, whereas now a single `NSLayoutConstraint.deactivateConstraints(_:)` takes place per `<-`, `easy_reload` or `easy_clear` +operation. + ## v.1.1.0 * Now it's possible to combine `multipliers` with `Equal`, `GreaterThatOrEqual` diff --git a/EasyPeasy.podspec b/EasyPeasy.podspec index fa860f8..53d0aee 100644 --- a/EasyPeasy.podspec +++ b/EasyPeasy.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "EasyPeasy" - s.version = "1.1.0" + s.version = "1.1.1" s.summary = "EasyPeasy is a Swift framework that eases the creation of Autolayout constraints programmatically" s.description = <<-DESC diff --git a/EasyPeasy/Item.swift b/EasyPeasy/Item.swift index 73bfcdf..aa2ea73 100644 --- a/EasyPeasy/Item.swift +++ b/EasyPeasy/Item.swift @@ -16,6 +16,13 @@ import AppKit internal var easy_attributesReference: Int = 0 +/** + Typealias of a tuple grouping an array of `NSLayoutConstraints` + to activate and another array of `NSLayoutConstraints` to + deactivate + */ +internal typealias ActivationGroup = ([NSLayoutConstraint], [NSLayoutConstraint]) + /** Protocol enclosing the objects a constraint will apply to */ @@ -38,12 +45,17 @@ public extension Item { closures will be evaluated again */ public func easy_reload() { - var layoutConstraints: [NSLayoutConstraint] = [] + var activateConstraints: [NSLayoutConstraint] = [] + var deactivateConstraints: [NSLayoutConstraint] = [] for node in self.nodes.values { - layoutConstraints.appendContentsOf(node.reload()) + let activationGroup = node.reload() + activateConstraints.appendContentsOf(activationGroup.0) + deactivateConstraints.appendContentsOf(activationGroup.1) } - // Activate the resulting `NSLayoutConstraints` - NSLayoutConstraint.activateConstraints(layoutConstraints) + + // Activate/deactivate the resulting `NSLayoutConstraints` + NSLayoutConstraint.deactivateConstraints(deactivateConstraints) + NSLayoutConstraint.activateConstraints(activateConstraints) } /** @@ -51,10 +63,14 @@ public extension Item { current `UIView` */ public func easy_clear() { + var deactivateConstraints: [NSLayoutConstraint] = [] for node in self.nodes.values { - node.clear() + deactivateConstraints.appendContentsOf(node.clear()) } self.nodes = [:] + + // Deactivate the resulting `NSLayoutConstraints` + NSLayoutConstraint.deactivateConstraints(deactivateConstraints) } } @@ -91,26 +107,30 @@ internal extension Item { */ internal func apply(attributes attributes: [Attribute]) -> [NSLayoutConstraint] { var layoutConstraints: [NSLayoutConstraint] = [] + var activateConstraints: [NSLayoutConstraint] = [] + var deactivateConstraints: [NSLayoutConstraint] = [] for attribute in attributes { - // if let compoundAttribute = attribute as? CompoundAttribute { layoutConstraints.appendContentsOf(self.apply(attributes: compoundAttribute.attributes)) continue } - // - let createdConstraints = self.apply(attribute: attribute) - layoutConstraints.appendContentsOf(createdConstraints) + if let activationGroup = self.apply(attribute: attribute) { + layoutConstraints.appendContentsOf(activationGroup.0) + activateConstraints.appendContentsOf(activationGroup.0) + deactivateConstraints.appendContentsOf(activationGroup.1) + } } - // Activate the `NSLayoutConstraints` returned by the different `Nodes` - NSLayoutConstraint.activateConstraints(layoutConstraints) + // Activate/deactivate the `NSLayoutConstraints` returned by the different `Nodes` + NSLayoutConstraint.deactivateConstraints(deactivateConstraints) + NSLayoutConstraint.activateConstraints(activateConstraints) return layoutConstraints } - internal func apply(attribute attribute: Attribute) -> [NSLayoutConstraint] { + internal func apply(attribute attribute: Attribute) -> ActivationGroup? { // Creates the `NSLayoutConstraint` of the `Attribute` holding // a reference to it from the `Attribute` objects attribute.createConstraints(for: self) @@ -119,14 +139,12 @@ internal extension Item { // in case it doesn't exist let node = self.nodes[attribute.signature] ?? Node() - // Add the `Attribute` to the node and appends the `NSLayoutConstraints` - // that have to be activated - let createdConstraints = node.add(attribute: attribute) - // Set node self.nodes[attribute.signature] = node - return createdConstraints + // Add the `Attribute` to the node and return the `NSLayoutConstraints` + // to be activated/deactivated + return node.add(attribute: attribute) } } diff --git a/EasyPeasy/Node.swift b/EasyPeasy/Node.swift index 513acff..f0cada3 100644 --- a/EasyPeasy/Node.swift +++ b/EasyPeasy/Node.swift @@ -62,36 +62,39 @@ internal class Node { `Node` and its associated `NSLayoutConstraint` returned in order to be activated by the `Item` owning the `Node`. - parameter attribute: `Attribute` to be added to the `Node` - - returns: `NSLayoutConstraints` to be activated by the - `Item` owning the current `Node` + - returns an `ActivationGroup` gathering `NSLayoutConstraints` + to be activated/deactivated by the `Item` owning the current + `Node` */ - func add(attribute attribute: Attribute) -> [NSLayoutConstraint] { + func add(attribute attribute: Attribute) -> ActivationGroup? { guard attribute.shouldInstall() else { self.inactiveAttributes.append(attribute) - return [] + return nil } + var deactivate: [NSLayoutConstraint]? + // Checks whether the `Attribute` is conflicting with any of // the existing `Subnodes`. If so deactivates the conflicting // `Subnodes` let nodeAttribute = attribute.createAttribute.subnode switch nodeAttribute { case .Left: - if self.left === attribute { return [] } - self.deactivate(attributes: [self.left, self.center].flatMap { $0 }) + if self.left === attribute { return nil } + deactivate = self.deactivate(attributes: [self.left, self.center].flatMap { $0 }) self.left = attribute case .Right: - if self.right === attribute { return [] } - self.deactivate(attributes: [self.right, self.center].flatMap { $0 }) + if self.right === attribute { return nil } + deactivate = self.deactivate(attributes: [self.right, self.center].flatMap { $0 }) self.right = attribute case .Center: - if self.center === attribute { return [] } - self.deactivate(attributes: [self.center, self.left, self.right].flatMap { $0 }) + if self.center === attribute { return nil } + deactivate = self.deactivate(attributes: [self.center, self.left, self.right].flatMap { $0 }) self.center = attribute case .Dimension: - if self.dimension === attribute { return [] } + if self.dimension === attribute { return nil } if let previousDimension = self.dimension { - self.deactivate(attributes: [previousDimension]) + deactivate = self.deactivate(attributes: [previousDimension]) } self.dimension = attribute } @@ -99,20 +102,26 @@ internal class Node { // Returns the associated `NSLayoutConstraint` to be activated // by the `Item` owning the `Node` if let layoutConstraint = attribute.layoutConstraint { - return [layoutConstraint] + return ([layoutConstraint], deactivate ?? []) + } + + // Return constraints to deactivate + if let deactivateConstraints = deactivate { + return ([], deactivateConstraints) } - return [] + return nil } /** - Deactivates the `NSLayoutConstraints` for the `Attributes` - given. Also nullifies the `Subnodes` for those `Attributes` + Returns the `NSLayoutConstraints` to deactivate for the `Attributes` + given. Also nullifies the `Subnodes` holding those `Attributes` - parameter attributes: `Attributes` to be deactivated + - returns an array of `NSLayoutConstraints` to be deactivated */ - func deactivate(attributes attributes: [Attribute]) { + func deactivate(attributes attributes: [Attribute]) -> [NSLayoutConstraint] { guard attributes.count > 0 else { - return + return [] } var layoutConstraints: [NSLayoutConstraint] = [] @@ -131,24 +140,27 @@ internal class Node { } } - // Deactivate `NSLayoutContraints` - NSLayoutConstraint.deactivateConstraints(layoutConstraints) + // Return `NSLayoutContraints` to deactivate + return layoutConstraints } /** - Re-evaluates every `Condition` closure within the active and - inactive `Attributes`, in case an active `Attribute` has - become inactive deactivates it and the `NSLayoutConstraints` of - those that have changed to active are passed to the `Item` owner - of the `Node` to active them along with other `Nodes` active - `NSLayoutConstraints` - - returns the `NSLayoutConstraints` to be activated + Re-evaluates every `Condition` closure within the active and inactive + `Attributes`, in case an active `Attribute` has become inactive + returns its associated `NSLayoutConstraints` along with those that + have changed to active in order to be activated or deactivated by the + `Item` owning the `Node` + - returns an `ActivationGroup` gathering the `NSLayoutConstraints` + to be activated and deactivated */ - func reload() -> [NSLayoutConstraint] { + func reload() -> ActivationGroup { + var activateConstraints: [NSLayoutConstraint] = [] + var deactivateConstraints: [NSLayoutConstraint] = [] - // Deactivate `Attributes` which condition changed to false + // Get the `Attributes` its condition changed to false in order to + // deactivate the associated `NSLayoutConstraint` let deactivatedAttributes = self.activeAttributes.filter { $0.shouldInstall() == false } - self.deactivate(attributes: deactivatedAttributes) + deactivateConstraints.appendContentsOf(self.deactivate(attributes: deactivatedAttributes)) // Gather all the existing `Attributes` that need to be added // again to the `Node` @@ -161,18 +173,26 @@ internal class Node { // Re-add `Attributes` to the `Node` in order to solve conflicts // and re-evaluate `Conditions` - let layoutAttributes = activeAttributes.flatMap { self.add(attribute: $0) } + activeAttributes.forEach { attribute in + if let activationGroup = self.add(attribute: attribute) { + activateConstraints.appendContentsOf(activationGroup.0) + deactivateConstraints.appendContentsOf(activationGroup.1) + } + } - return layoutAttributes + return (activateConstraints, deactivateConstraints) } /** - Deactives all the active `Attributes` within the node and - clears all the persisted ones + Returns all the active `NSLayoutConstraints` within the node and + clears all the persisted `Attributes` + - returns an array of `NSLayoutConstraints` to deactivate */ - func clear() { - self.deactivate(attributes: self.activeAttributes) + func clear() -> [NSLayoutConstraint] { + let deactivateConstraints = self.deactivate(attributes: self.activeAttributes) self.inactiveAttributes = [] + + return deactivateConstraints } } diff --git a/Example/Tests/NodeTests.swift b/Example/Tests/NodeTests.swift index 4077e6f..12dc764 100644 --- a/Example/Tests/NodeTests.swift +++ b/Example/Tests/NodeTests.swift @@ -125,13 +125,14 @@ class NodeTests: XCTestCase { let leftAttribute = Left() leftAttribute.createConstraints(for: view) let constraints = node.add(attribute: leftAttribute) - XCTAssertTrue(constraints.count == 1) + XCTAssertTrue(constraints?.0.count == 1) + XCTAssertTrue(constraints?.1.count == 0) // when let newConstraints = node.add(attribute: leftAttribute) // then - XCTAssertTrue(newConstraints.count == 0) + XCTAssertNil(newConstraints) } // MARK: Right node @@ -246,13 +247,14 @@ class NodeTests: XCTestCase { let rightAttribute = Right() rightAttribute.createConstraints(for: view) let constraints = node.add(attribute: rightAttribute) - XCTAssertTrue(constraints.count == 1) + XCTAssertTrue(constraints?.0.count == 1) + XCTAssertTrue(constraints?.1.count == 0) // when let newConstraints = node.add(attribute: rightAttribute) // then - XCTAssertTrue(newConstraints.count == 0) + XCTAssertNil(newConstraints) } // MARK: Center node @@ -368,13 +370,14 @@ class NodeTests: XCTestCase { let centerAttribute = CenterX() centerAttribute.createConstraints(for: view) let constraints = node.add(attribute: centerAttribute) - XCTAssertTrue(constraints.count == 1) + XCTAssertTrue(constraints?.0.count == 1) + XCTAssertTrue(constraints?.1.count == 0) // when let newConstraints = node.add(attribute: centerAttribute) // then - XCTAssertTrue(newConstraints.count == 0) + XCTAssertNil(newConstraints) } // MARK: Dimension node @@ -467,25 +470,32 @@ class NodeTests: XCTestCase { let widthAttribute = Width() widthAttribute.createConstraints(for: view) let constraints = node.add(attribute: widthAttribute) - XCTAssertTrue(constraints.count == 1) + XCTAssertTrue(constraints?.0.count == 1) + XCTAssertTrue(constraints?.1.count == 0) // when let newConstraints = node.add(attribute: widthAttribute) // then - XCTAssertTrue(newConstraints.count == 0) + XCTAssertNil(newConstraints) } func testThatReloadHandlesCorrectlyEachSubnode() { // given + let superview = UIView() + let view = UIView() + superview.addSubview(view) var value = true let node = Node() let leftAttributeA = LeftMargin().when { value } + leftAttributeA.createConstraints(for: view) let leftAttributeB = Left().when { value == false } + leftAttributeB.createConstraints(for: view) let rightAttribute = RightMargin() - node.add(attribute: leftAttributeA) - node.add(attribute: leftAttributeB) - node.add(attribute: rightAttribute) + rightAttribute.createConstraints(for: view) + let activationGroupA = node.add(attribute: leftAttributeA) + let activationGroupB = node.add(attribute: leftAttributeB) + let activationGroupC = node.add(attribute: rightAttribute) let activeAttributes = node.activeAttributes let inactiveAttributes = node.inactiveAttributes @@ -496,10 +506,15 @@ class NodeTests: XCTestCase { XCTAssertTrue(node.right === rightAttribute) XCTAssertTrue(inactiveAttributes.first === leftAttributeB) XCTAssertNil(node.center) + XCTAssertTrue(activationGroupA?.0.count == 1) + XCTAssertTrue(activationGroupA?.1.count == 0) + XCTAssertNil(activationGroupB) + XCTAssertTrue(activationGroupC?.0.count == 1) + XCTAssertTrue(activationGroupC?.1.count == 0) // when value = false - node.reload() + let reloadActivationGroup = node.reload() // then XCTAssertTrue(node.activeAttributes.count == 2) @@ -508,12 +523,14 @@ class NodeTests: XCTestCase { XCTAssertTrue(node.right === rightAttribute) XCTAssertTrue(inactiveAttributes.first === leftAttributeB) XCTAssertNil(node.center) + XCTAssertTrue(reloadActivationGroup.0.count == 1) + XCTAssertTrue(reloadActivationGroup.1.count == 1) // And again // when value = true - node.reload() + let reloadActivationGroupB = node.reload() // then XCTAssertTrue(node.activeAttributes.count == 2) @@ -522,16 +539,26 @@ class NodeTests: XCTestCase { XCTAssertTrue(node.right === rightAttribute) XCTAssertTrue(inactiveAttributes.first === leftAttributeB) XCTAssertNil(node.center) + XCTAssertTrue(reloadActivationGroupB.0.count == 1) + XCTAssertTrue(reloadActivationGroupB.1.count == 1) } - func testThatClearMethodRemovesEverySubnode() { + func testThatClearMethodRemovesEverySubnodeAndReturnsTheExpectedConstraints() { // given + let superview = UIView() + let view = UIView() + superview.addSubview(view) let node = Node() let leftAttributeA = TopMargin().when { true } + leftAttributeA.createConstraints(for: view) let leftAttributeB = Top().when { false } + leftAttributeB.createConstraints(for: view) let rightAttribute = LastBaseline() + rightAttribute.createConstraints(for: view) let dimension = Width() + dimension.createConstraints(for: view) let center = CenterXWithinMargins().when { false } + center.createConstraints(for: view) node.add(attribute: leftAttributeA) node.add(attribute: leftAttributeB) node.add(attribute: rightAttribute) @@ -546,7 +573,7 @@ class NodeTests: XCTestCase { XCTAssertNil(node.center) // when - node.clear() + let constraints = node.clear() // then XCTAssertNil(node.left) @@ -555,6 +582,150 @@ class NodeTests: XCTestCase { XCTAssertNil(node.center) XCTAssertTrue(node.activeAttributes.count == 0) XCTAssertTrue(node.inactiveAttributes.count == 0) + XCTAssertTrue(constraints.count == 3) + } + + func testThatActivationGroupIsTheExpectedAddingAttributes() { + // given + let superview = UIView() + let view = UIView() + superview.addSubview(view) + let node = Node() + + let leftAttributeA = Left(100) + leftAttributeA.createConstraints(for: view) + let rightAttributeA = Right(100) + rightAttributeA.createConstraints(for: view) + + let activationGroupLeftA = node.add(attribute: leftAttributeA) + XCTAssertTrue(activationGroupLeftA!.0.count == 1) + XCTAssertTrue(activationGroupLeftA!.0.first === leftAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupLeftA!.1.count == 0) + + let activationGroupRightA = node.add(attribute: rightAttributeA) + XCTAssertTrue(activationGroupRightA!.0.count == 1) + XCTAssertTrue(activationGroupRightA!.0.first === rightAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupRightA!.1.count == 0) + + // when + let centerAttribute = CenterX(0.0) + centerAttribute.createConstraints(for: view) + let activationGroupCenter = node.add(attribute: centerAttribute) + + // then + XCTAssertTrue(activationGroupCenter!.0.count == 1) + XCTAssertTrue(activationGroupCenter!.0.first === centerAttribute.layoutConstraint) + XCTAssertTrue(activationGroupCenter!.1.count == 2) + XCTAssertTrue(activationGroupCenter!.1[0] === leftAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupCenter!.1[1] === rightAttributeA.layoutConstraint) + } + + func testThatActivationGroupIsTheExpectedWhenSameAttributeIsAppliedTwice() { + // given + let superview = UIView() + let view = UIView() + superview.addSubview(view) + let node = Node() + + let leftAttributeA = Left(100) + leftAttributeA.createConstraints(for: view) + + let activationGroupLeftA = node.add(attribute: leftAttributeA) + XCTAssertTrue(activationGroupLeftA!.0.count == 1) + XCTAssertTrue(activationGroupLeftA!.0.first === leftAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupLeftA!.1.count == 0) + + // when + let activationGroupLeftB = node.add(attribute: leftAttributeA) + + // then + XCTAssertNil(activationGroupLeftB) + } + + func testThatActivationGroupIsTheExpectedWhenShouldInstallIsFalse() { + // given + let superview = UIView() + let view = UIView() + superview.addSubview(view) + let node = Node() + + let leftAttributeA = Left(100).when { false } + leftAttributeA.createConstraints(for: view) + + // when + let activationGroupLeftA = node.add(attribute: leftAttributeA) + + // then + XCTAssertNil(activationGroupLeftA) + } + + func testThatActivationGroupIsTheExpectedUponReloadWithNoChanges() { + // given + let superview = UIView() + let view = UIView() + superview.addSubview(view) + let node = Node() + + let leftAttributeA = Left(100) + leftAttributeA.createConstraints(for: view) + let rightAttributeA = Right(100) + rightAttributeA.createConstraints(for: view) + + let activationGroupLeftA = node.add(attribute: leftAttributeA) + XCTAssertTrue(activationGroupLeftA!.0.count == 1) + XCTAssertTrue(activationGroupLeftA!.0.first === leftAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupLeftA!.1.count == 0) + + let activationGroupRightA = node.add(attribute: rightAttributeA) + XCTAssertTrue(activationGroupRightA!.0.count == 1) + XCTAssertTrue(activationGroupRightA!.0.first === rightAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupRightA!.1.count == 0) + + // when + let activationGroupReload = node.reload() + + // then + XCTAssertTrue(activationGroupReload.0.count == 0) + XCTAssertTrue(activationGroupReload.1.count == 0) + } + + func testThatActivationGroupIsTheExpectedUponReloadWithChanges() { + // given + let superview = UIView() + let view = UIView() + superview.addSubview(view) + let node = Node() + var condition = true + + let leftAttributeA = Left(100).when { condition } + leftAttributeA.createConstraints(for: view) + let leftAttributeB = Left(100).when { !condition } + leftAttributeB.createConstraints(for: view) + let rightAttributeA = Right(100) + rightAttributeA.createConstraints(for: view) + + let activationGroupLeftA = node.add(attribute: leftAttributeA) + XCTAssertTrue(activationGroupLeftA!.0.count == 1) + XCTAssertTrue(activationGroupLeftA!.0.first === leftAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupLeftA!.1.count == 0) + + let activationGroupLeftB = node.add(attribute: leftAttributeB) + XCTAssertNil(activationGroupLeftB) + + let activationGroupRightA = node.add(attribute: rightAttributeA) + XCTAssertTrue(activationGroupRightA!.0.count == 1) + XCTAssertTrue(activationGroupRightA!.0.first === rightAttributeA.layoutConstraint) + XCTAssertTrue(activationGroupRightA!.1.count == 0) + + // when + condition = false + let activationGroupReload = node.reload() + + // then + XCTAssertTrue(activationGroupReload.0.count == 1) + XCTAssertTrue(activationGroupReload.0.first === leftAttributeB.layoutConstraint) + XCTAssertTrue(activationGroupReload.1.count == 1) + XCTAssertTrue(activationGroupReload.1.first === leftAttributeA.layoutConstraint) } } diff --git a/Example/Tests/UIView+EasyTests.swift b/Example/Tests/UIView+EasyTests.swift index dab18a2..c09e764 100644 --- a/Example/Tests/UIView+EasyTests.swift +++ b/Example/Tests/UIView+EasyTests.swift @@ -202,4 +202,46 @@ class UIView_EasyTests: XCTestCase { XCTAssertTrue(constraints.count == 0) } + func testThatConstraintsAreTheExpectedUponEasyClear() { + // given + let superview = UIView(frame: CGRectMake(0, 0, 400, 1000)) + let viewA = UIView(frame: CGRectZero) + superview.addSubview(viewA) + viewA <- [ Left(), Size(20), Top() ] + XCTAssertTrue(superview.constraints.count == 2) + XCTAssertTrue(viewA.constraints.count == 2) + + // when + viewA.easy_clear() + + // then + XCTAssertTrue(superview.constraints.count == 0) + XCTAssertTrue(viewA.constraints.count == 0) + } + + func testThatConstraintsAreTheExpectedUponEasyReload() { + // given + var condition = true + let superview = UIView(frame: CGRectMake(0, 0, 400, 1000)) + let viewA = UIView(frame: CGRectZero) + superview.addSubview(viewA) + viewA <- [ + Left().when { condition }, + Right().when { condition }, + CenterX().when { !condition }, + Size(20), + Top() + ] + XCTAssertTrue(superview.constraints.count == 3) + XCTAssertTrue(viewA.constraints.count == 2) + + // when + condition = false + viewA.easy_reload() + + // then + XCTAssertTrue(superview.constraints.count == 2) + XCTAssertTrue(viewA.constraints.count == 2) + } + }