From b448ae8739f8fa070ac0b67a4ac269dd7017936b Mon Sep 17 00:00:00 2001 From: CosynPa Date: Sun, 15 Nov 2015 23:43:20 +0800 Subject: [PATCH 1/4] Separate different kinds of spacer views --- TZStackView/TZSpacerView.swift | 2 +- TZStackView/TZStackView.swift | 178 ++++++++++++++++++--------------- 2 files changed, 99 insertions(+), 81 deletions(-) mode change 100755 => 100644 TZStackView/TZStackView.swift diff --git a/TZStackView/TZSpacerView.swift b/TZStackView/TZSpacerView.swift index bb8e9b9..3fad8ec 100644 --- a/TZStackView/TZSpacerView.swift +++ b/TZStackView/TZSpacerView.swift @@ -9,5 +9,5 @@ import UIKit public class TZSpacerView: UIView { - + public var identifier: String = "" } diff --git a/TZStackView/TZStackView.swift b/TZStackView/TZStackView.swift old mode 100755 new mode 100644 index 2fd77a8..4421d96 --- a/TZStackView/TZStackView.swift +++ b/TZStackView/TZStackView.swift @@ -48,8 +48,10 @@ public class TZStackView: UIView { private var stackViewConstraints = [NSLayoutConstraint]() private var subviewConstraints = [NSLayoutConstraint]() - - private var spacerViews = [UIView]() + + private var layoutMarginsView: TZSpacerView? + private var alignmentSpanner: TZSpacerView? + private var distributionSpacers: [TZSpacerView] = [] private var animationDidStopQueueEntries = [TZAnimationDidStopQueueEntry]() @@ -177,6 +179,22 @@ public class TZStackView: UIView { arrangedSubview.removeConstraints(subviewConstraints) } subviewConstraints.removeAll() + + if let spacerView = layoutMarginsView { + spacerView.removeFromSuperview() + layoutMarginsView = nil + } + + if let spacerView = alignmentSpanner { + spacerView.removeFromSuperview() + alignmentSpanner = nil + } + + for spacerView in distributionSpacers { + spacerView.removeFromSuperview() + } + distributionSpacers.removeAll() + for arrangedSubview in arrangedSubviews { if alignment != .Fill { @@ -204,24 +222,22 @@ public class TZStackView: UIView { } } - for spacerView in spacerViews { - spacerView.removeFromSuperview() - } - spacerViews.removeAll() - if arrangedSubviews.count > 0 { + if layoutMarginsRelativeArrangement { + layoutMarginsView = addSpacerView("TZViewLayoutMarginsGuide") + } + + if alignment != .Fill { + alignmentSpanner = addSpacerView("TZSV-alignment-spanner") + } + + stackViewConstraints += createMatchEdgesContraints(arrangedSubviews) + stackViewConstraints += createFirstAndLastViewMatchEdgesContraints() let visibleArrangedSubviews = arrangedSubviews.filter({!self.isHidden($0)}) switch distribution { case .FillEqually, .Fill, .FillProportionally: - if alignment != .Fill || layoutMarginsRelativeArrangement { - addSpacerView() - } - - stackViewConstraints += createMatchEdgesContraints(arrangedSubviews) - stackViewConstraints += createFirstAndLastViewMatchEdgesContraints() - if alignment == .FirstBaseline && axis == .Horizontal { stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49)) } @@ -242,17 +258,13 @@ public class TZStackView: UIView { continue } if index > 0 { - views.append(addSpacerView()) + let spacerView = addSpacerView("TZSV-distributing") + distributionSpacers.append(spacerView) + views.append(spacerView) } views.append(arrangedSubview) index++ } - if spacerViews.count == 0 { - addSpacerView() - } - - stackViewConstraints += createMatchEdgesContraints(arrangedSubviews) - stackViewConstraints += createFirstAndLastViewMatchEdgesContraints() switch axis { case .Horizontal: @@ -265,20 +277,14 @@ public class TZStackView: UIView { } stackViewConstraints += createFillConstraints(views, constant: 0) - stackViewConstraints += createFillEquallyConstraints(spacerViews) + stackViewConstraints += createFillEquallyConstraints(distributionSpacers) stackViewConstraints += createFillConstraints(arrangedSubviews, relatedBy: .GreaterThanOrEqual, constant: spacing) case .EqualCentering: for (index, _) in visibleArrangedSubviews.enumerate() { if index > 0 { - addSpacerView() + distributionSpacers.append(addSpacerView("TZSV-distributing")) } } - if spacerViews.count == 0 { - addSpacerView() - } - - stackViewConstraints += createMatchEdgesContraints(arrangedSubviews) - stackViewConstraints += createFirstAndLastViewMatchEdgesContraints() switch axis { case .Horizontal: @@ -293,7 +299,7 @@ public class TZStackView: UIView { var previousArrangedSubview: UIView? for (index, arrangedSubview) in visibleArrangedSubviews.enumerate() { if let previousArrangedSubview = previousArrangedSubview { - let spacerView = spacerViews[index - 1] + let spacerView = distributionSpacers[index - 1] switch axis { case .Horizontal: @@ -307,22 +313,21 @@ public class TZStackView: UIView { previousArrangedSubview = arrangedSubview } - stackViewConstraints += createFillEquallyConstraints(spacerViews, priority: 150) + stackViewConstraints += createFillEquallyConstraints(distributionSpacers, priority: 150) stackViewConstraints += createFillConstraints(arrangedSubviews, relatedBy: .GreaterThanOrEqual, constant: spacing) } - if spacerViews.count > 0 { - stackViewConstraints += createSurroundingSpacerViewConstraints(spacerViews[0], views: visibleArrangedSubviews) + if let spanner = alignmentSpanner { + stackViewConstraints += createSurroundingSpacerViewConstraints(spanner, views: visibleArrangedSubviews) } - if layoutMarginsRelativeArrangement { - if spacerViews.count > 0 { - stackViewConstraints.append(constraint(item: self, attribute: .BottomMargin, toItem: spacerViews[0])) - stackViewConstraints.append(constraint(item: self, attribute: .LeftMargin, toItem: spacerViews[0])) - stackViewConstraints.append(constraint(item: self, attribute: .RightMargin, toItem: spacerViews[0])) - stackViewConstraints.append(constraint(item: self, attribute: .TopMargin, toItem: spacerViews[0])) - } + if let layoutMarginsView = layoutMarginsView { + stackViewConstraints.append(constraint(item: self, attribute: .BottomMargin, toItem: layoutMarginsView)) + stackViewConstraints.append(constraint(item: self, attribute: .LeftMargin, toItem: layoutMarginsView)) + stackViewConstraints.append(constraint(item: self, attribute: .RightMargin, toItem: layoutMarginsView)) + stackViewConstraints.append(constraint(item: self, attribute: .TopMargin, toItem: layoutMarginsView)) } + addConstraints(stackViewConstraints) } @@ -333,11 +338,11 @@ public class TZStackView: UIView { super.init(coder: aDecoder)! } - private func addSpacerView() -> TZSpacerView { + private func addSpacerView(identifier: String = "") -> TZSpacerView { let spacerView = TZSpacerView() spacerView.translatesAutoresizingMaskIntoConstraints = false + spacerView.identifier = identifier - spacerViews.append(spacerView) insertSubview(spacerView, atIndex: 0) return spacerView } @@ -509,58 +514,71 @@ public class TZStackView: UIView { let visibleViews = arrangedSubviews.filter({!self.isHidden($0)}) let firstView = visibleViews.first let lastView = visibleViews.last - - var topView = arrangedSubviews.first! - var bottomView = arrangedSubviews.first! - if spacerViews.count > 0 { - if alignment == .Center { - topView = spacerViews[0] - bottomView = spacerViews[0] - } else if alignment == .Top || alignment == .Leading { - bottomView = spacerViews[0] - } else if alignment == .Bottom || alignment == .Trailing { - topView = spacerViews[0] - } else if alignment == .FirstBaseline { - switch axis { - case .Horizontal: - bottomView = spacerViews[0] - case .Vertical: - topView = spacerViews[0] - bottomView = spacerViews[0] - } - } - } - let firstItem = layoutMarginsRelativeArrangement ? spacerViews[0] : self - + let edgeItem = layoutMarginsView ?? self + switch axis { case .Horizontal: if let firstView = firstView { - constraints.append(constraint(item: firstItem, attribute: .Leading, toItem: firstView)) + constraints.append(constraint(item: edgeItem, attribute: .Leading, toItem: firstView)) } if let lastView = lastView { - constraints.append(constraint(item: firstItem, attribute: .Trailing, toItem: lastView)) - } - - constraints.append(constraint(item: firstItem, attribute: .Top, toItem: topView)) - constraints.append(constraint(item: firstItem, attribute: .Bottom, toItem: bottomView)) - - if alignment == .Center { - constraints.append(constraint(item: firstItem, attribute: .CenterY, toItem: arrangedSubviews.first!)) + constraints.append(constraint(item: edgeItem, attribute: .Trailing, toItem: lastView)) } case .Vertical: if let firstView = firstView { - constraints.append(constraint(item: firstItem, attribute: .Top, toItem: firstView)) + constraints.append(constraint(item: edgeItem, attribute: .Top, toItem: firstView)) } if let lastView = lastView { - constraints.append(constraint(item: firstItem, attribute: .Bottom, toItem: lastView)) + constraints.append(constraint(item: edgeItem, attribute: .Bottom, toItem: lastView)) } + } + + let firstArrangedView = arrangedSubviews.first! + + let topView: UIView + let bottomView: UIView + var centerView: UIView? + + switch alignment { + case .Fill: + topView = firstArrangedView + bottomView = firstArrangedView + case .Center: + topView = alignmentSpanner! + bottomView = alignmentSpanner! + centerView = firstArrangedView + case .Leading, .Top: + topView = firstArrangedView + bottomView = alignmentSpanner! + case .Trailing, .Bottom: + topView = alignmentSpanner! + bottomView = firstArrangedView + case .FirstBaseline: + switch axis { + case .Horizontal: + topView = firstArrangedView + bottomView = alignmentSpanner! + case .Vertical: + topView = alignmentSpanner! + bottomView = alignmentSpanner! + } + } + + switch axis { + case .Horizontal: + constraints.append(constraint(item: edgeItem, attribute: .Top, toItem: topView)) + constraints.append(constraint(item: edgeItem, attribute: .Bottom, toItem: bottomView)) - constraints.append(constraint(item: firstItem, attribute: .Leading, toItem: topView)) - constraints.append(constraint(item: firstItem, attribute: .Trailing, toItem: bottomView)) + if let centerView = centerView { + constraints.append(constraint(item: edgeItem, attribute: .CenterY, toItem: centerView)) + } + case .Vertical: + constraints.append(constraint(item: edgeItem, attribute: .Leading, toItem: topView)) + constraints.append(constraint(item: edgeItem, attribute: .Trailing, toItem: bottomView)) - if alignment == .Center { - constraints.append(constraint(item: firstItem, attribute: .CenterX, toItem: arrangedSubviews.first!)) + if let centerView = centerView { + constraints.append(constraint(item: edgeItem, attribute: .CenterX, toItem: centerView)) } } From f1aaa08797fa61dfbaa222bdff709d1a3548372f Mon Sep 17 00:00:00 2001 From: CosynPa Date: Sun, 15 Nov 2015 23:46:18 +0800 Subject: [PATCH 2/4] Test whether spacer guides are the same --- TZStackViewTests/TZStackViewTestCase.swift | 40 ++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/TZStackViewTests/TZStackViewTestCase.swift b/TZStackViewTests/TZStackViewTestCase.swift index 8fcf3f0..2045adf 100644 --- a/TZStackViewTests/TZStackViewTestCase.swift +++ b/TZStackViewTests/TZStackViewTestCase.swift @@ -119,6 +119,28 @@ class TZStackViewTestCase: XCTestCase { return } + func getGuides(constraints: [NSLayoutConstraint]) -> [NSObject] { + var result = Set() + + for aConstraint in constraints { + let firstItem = aConstraint.firstItem + if firstItem is TZSpacerView || firstItem is UILayoutGuide { + result.insert(firstItem as! NSObject) + } + + if let secondItem = aConstraint.secondItem where secondItem is TZSpacerView || secondItem is UILayoutGuide { + result.insert(secondItem as! NSObject) + } + } + + return Array(result) + } + + let uiGuides = getGuides(uiConstraints) + let tzGuides = getGuides(tzConstraints) + + XCTAssertEqual(uiGuides.count, tzGuides.count, "Number of layout guides") + for (index, uiConstraint) in uiConstraints.enumerate() { let tzConstraint = tzConstraints[index] @@ -197,15 +219,29 @@ class TZStackViewTestCase: XCTestCase { return true } // Wish I could assert more accurately than this - if object1 is UILayoutGuide && object2 is TZSpacerView { + if let object1 = object1 as? UILayoutGuide, object2 = object2 as? TZSpacerView + where isSameIdentifier(object1.identifier, object2.identifier) { return true } // Wish I could assert more accurately than this - if object1 is TZSpacerView && object2 is UILayoutGuide { + if let object1 = object1 as? TZSpacerView, object2 = object2 as? UILayoutGuide + where isSameIdentifier(object1.identifier, object2.identifier) { return true } return false } + + private func isSameIdentifier(identifier1: String, _ identifier2: String) -> Bool { + func hasPrefix(str: String) -> Bool { + return str.hasPrefix("UI") || str.hasPrefix("TZ") + } + + func dropPrefix(str: String) -> String { + return String(str.characters.dropFirst("UI".characters.count)) + } + + return identifier1 == identifier2 || (hasPrefix(identifier1) && hasPrefix(identifier2) && dropPrefix(identifier1) == dropPrefix(identifier2)) + } func assertSameOrder(uiTestViews: [TestView], _ tzTestViews: [TestView]) { for (index, uiTestView) in uiTestViews.enumerate() { From a0d84b902dfdfc23d6f1057c836fdfb7fccfcc1a Mon Sep 17 00:00:00 2001 From: CosynPa Date: Sun, 22 Nov 2015 00:03:59 +0800 Subject: [PATCH 3/4] Implement layoutMarginsRelativeArrangement --- TZStackView/TZStackView.swift | 49 +++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/TZStackView/TZStackView.swift b/TZStackView/TZStackView.swift index 4421d96..45d41f6 100644 --- a/TZStackView/TZStackView.swift +++ b/TZStackView/TZStackView.swift @@ -36,6 +36,26 @@ public class TZStackView: UIView { public var spacing: CGFloat = 0 public var layoutMarginsRelativeArrangement = false + + override public var layoutMargins: UIEdgeInsets { + get { + if #available(iOS 8, *) { + return super.layoutMargins + } else { + return _layoutMargins + } + } + set { + if #available(iOS 8, *) { + super.layoutMargins = newValue + } else { + _layoutMargins = newValue + setNeedsUpdateConstraints() + } + } + } + + private var _layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) public private(set) var arrangedSubviews: [UIView] = [] { didSet { @@ -322,10 +342,27 @@ public class TZStackView: UIView { } if let layoutMarginsView = layoutMarginsView { - stackViewConstraints.append(constraint(item: self, attribute: .BottomMargin, toItem: layoutMarginsView)) - stackViewConstraints.append(constraint(item: self, attribute: .LeftMargin, toItem: layoutMarginsView)) - stackViewConstraints.append(constraint(item: self, attribute: .RightMargin, toItem: layoutMarginsView)) - stackViewConstraints.append(constraint(item: self, attribute: .TopMargin, toItem: layoutMarginsView)) + let bottomConstraint: NSLayoutConstraint + let leftConstraint: NSLayoutConstraint + let rightConstraint: NSLayoutConstraint + let topConstraint: NSLayoutConstraint + if #available(iOS 8.0, *) { + bottomConstraint = constraint(item: self, attribute: .BottomMargin, toItem: layoutMarginsView, attribute: .Bottom) + leftConstraint = constraint(item: self, attribute: .LeftMargin, toItem: layoutMarginsView, attribute: .Left) + rightConstraint = constraint(item: self, attribute: .RightMargin, toItem: layoutMarginsView, attribute: .Right) + topConstraint = constraint(item: self, attribute: .TopMargin, toItem: layoutMarginsView, attribute: .Top) + } else { + bottomConstraint = constraint(item: self, attribute: .Bottom, toItem: layoutMarginsView, attribute: .Bottom, constant: _layoutMargins.bottom) + leftConstraint = constraint(item: self, attribute: .Left, toItem: layoutMarginsView, attribute: .Left, constant: -_layoutMargins.left) + rightConstraint = constraint(item: self, attribute: .Right, toItem: layoutMarginsView, attribute: .Right, constant: _layoutMargins.right) + topConstraint = constraint(item: self, attribute: .Top, toItem: layoutMarginsView, attribute: .Top, constant: -_layoutMargins.top) + } + + bottomConstraint.identifier = "TZView-bottomMargin-guide-constraint" + leftConstraint.identifier = "TZView-leftMargin-guide-constraint" + rightConstraint.identifier = "TZView-rightMargin-guide-constraint" + topConstraint.identifier = "TZView-topMargin-guide-constraint" + stackViewConstraints += [bottomConstraint, leftConstraint, rightConstraint, topConstraint] } addConstraints(stackViewConstraints) @@ -486,7 +523,9 @@ public class TZStackView: UIView { case .Trailing, .Bottom: constraints += equalAttributes(views: views, attribute: .Bottom) case .FirstBaseline: - constraints += equalAttributes(views: views, attribute: .FirstBaseline) + if #available(iOS 8.0, *) { + constraints += equalAttributes(views: views, attribute: .FirstBaseline) + } } case .Vertical: From 1a69b805850ffaaccdd69ebfdbd25f4e4fc4884e Mon Sep 17 00:00:00 2001 From: CosynPa Date: Sun, 22 Nov 2015 00:04:15 +0800 Subject: [PATCH 4/4] Test layoutMarginsRelativeArrangement --- TZStackViewTests/TZStackViewTestCase.swift | 12 +++++++++++- TZStackViewTests/TZStackViewTests.swift | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/TZStackViewTests/TZStackViewTestCase.swift b/TZStackViewTests/TZStackViewTestCase.swift index 2045adf..b20da30 100644 --- a/TZStackViewTests/TZStackViewTestCase.swift +++ b/TZStackViewTests/TZStackViewTestCase.swift @@ -96,7 +96,7 @@ class TZStackViewTestCase: XCTestCase { logAllConstraints() } // Assert same constraints are created - assertSameConstraints(uiStackView.constraints, tzStackView.constraints) + assertSameConstraints(nonMarginsLayoutConstraints(uiStackView), nonMarginsLayoutConstraints(tzStackView)) for (index, uiArrangedSubview) in uiStackView.arrangedSubviews.enumerate() { let tzArrangedSubview = tzStackView.arrangedSubviews[index] @@ -113,6 +113,16 @@ class TZStackViewTestCase: XCTestCase { return view.constraints.filter({ "\($0.dynamicType)" != "NSContentSizeLayoutConstraint" }) } + private func nonMarginsLayoutConstraints(view: UIView) -> [NSLayoutConstraint] { + return view.constraints.filter { aConstraint in + if let identifier = aConstraint.identifier { + return !identifier.hasSuffix("Margin-guide-constraint") + } else { + return true + } + } + } + func assertSameConstraints(uiConstraints: [NSLayoutConstraint], _ tzConstraints: [NSLayoutConstraint]) { XCTAssertEqual(uiConstraints.count, tzConstraints.count, "Number of constraints") if uiConstraints.count != tzConstraints.count { diff --git a/TZStackViewTests/TZStackViewTests.swift b/TZStackViewTests/TZStackViewTests.swift index 145d742..9e2a267 100644 --- a/TZStackViewTests/TZStackViewTests.swift +++ b/TZStackViewTests/TZStackViewTests.swift @@ -33,7 +33,7 @@ class TZStackViewTests: TZStackViewTestCase { } func testSameConstraints() { - let margins = [false] + let margins = [false, true] let axes = [ (UILayoutConstraintAxis.Horizontal, "Horizontal"), (.Vertical, "Vertical")