Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A wonderful layout component called the [`UIStackView` was introduced with *iOS
`UIStackView` requires *iOS 9*, but we're not ready to make our apps require *iOS 9+* just yet. In the meanwhile, we developers are eager to try this component in our apps right now! This is why I created this replica of the `UIStackView`, called the `TZStackView` (TZ = Tom van Zummeren, my initials). I created this component very carefully, tested every single corner case and matched the results against the *real* `UIStackView` with automated `XCTestCases`.

## Features
- ✅ Compatible with **iOS 7.x** and **iOS 8.x**
- ✅ Compatible with **iOS 7.x** or later
- ✅ Supports the complete API of `UIStackView` including **all** *distribution* and *alignment* options
- ✅ Supports animating the `hidden` property of the *arranged subviews*
- ❌ Supports *Storyboard*
Expand Down
17 changes: 9 additions & 8 deletions TZStackView.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
A45441C21B9B6D71002452BA /* TZStackView.h in Headers */ = {isa = PBXBuildFile; fileRef = A45441C11B9B6D71002452BA /* TZStackView.h */; settings = {ATTRIBUTES = (Public, ); }; };
A45441C61B9B6D71002452BA /* TZStackView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A45441BF1B9B6D71002452BA /* TZStackView.framework */; };
A45441C71B9B6D71002452BA /* TZStackView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A45441BF1B9B6D71002452BA /* TZStackView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A45441D01B9B6D9C002452BA /* TZSpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CC1B9B6D9C002452BA /* TZSpacerView.swift */; settings = {ASSET_TAGS = (); }; };
A45441D11B9B6D9C002452BA /* TZStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CD1B9B6D9C002452BA /* TZStackView.swift */; settings = {ASSET_TAGS = (); }; };
A45441D21B9B6D9C002452BA /* TZStackViewAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */; settings = {ASSET_TAGS = (); }; };
A45441D31B9B6D9C002452BA /* TZStackViewDistribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */; settings = {ASSET_TAGS = (); }; };
A45441D01B9B6D9C002452BA /* TZSpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CC1B9B6D9C002452BA /* TZSpacerView.swift */; };
A45441D11B9B6D9C002452BA /* TZStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CD1B9B6D9C002452BA /* TZStackView.swift */; };
A45441D21B9B6D9C002452BA /* TZStackViewAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */; };
A45441D31B9B6D9C002452BA /* TZStackViewDistribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */; };
DB41AF6A1B294B8E003DB902 /* NSLayoutConstraintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */; };
DB5B70851B2A1963006043BD /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B70841B2A1963006043BD /* TestView.swift */; };
DB5B70871B2B8816006043BD /* TZStackViewTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B70861B2B8816006043BD /* TZStackViewTestCase.swift */; };
Expand Down Expand Up @@ -123,6 +123,7 @@
5F50EB61965EE1FD3F76FB91 /* Products */,
);
sourceTree = "<group>";
usesTabs = 0;
};
5F50E7526ADB7151E0540D2D /* TZStackViewTests */ = {
isa = PBXGroup;
Expand Down Expand Up @@ -404,7 +405,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -442,7 +443,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
Expand All @@ -455,6 +456,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
INFOPLIST_FILE = TZStackViewDemo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "nl.tomvanzummeren.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -499,6 +501,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
INFOPLIST_FILE = TZStackViewDemo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "nl.tomvanzummeren.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -517,7 +520,6 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = TZStackView/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = nl.tomvanzummeren.TZStackView;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -540,7 +542,6 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = TZStackView/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = nl.tomvanzummeren.TZStackView;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
134 changes: 82 additions & 52 deletions TZStackView/TZStackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ public class TZStackView: UIView {
guideConstraint = constraint(item: arrangedSubview, attribute: .Width, toItem: nil, attribute: .NotAnAttribute, constant: 0, priority: 25)
}
subviewConstraints.append(guideConstraint)
arrangedSubview.addConstraint(guideConstraint)
}

if isHidden(arrangedSubview) {
Expand All @@ -200,7 +199,6 @@ public class TZStackView: UIView {
hiddenConstraint = constraint(item: arrangedSubview, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, constant: 0)
}
subviewConstraints.append(hiddenConstraint)
arrangedSubview.addConstraint(hiddenConstraint)
}
}

Expand All @@ -222,8 +220,10 @@ public class TZStackView: UIView {
stackViewConstraints += createMatchEdgesContraints(arrangedSubviews)
stackViewConstraints += createFirstAndLastViewMatchEdgesContraints()

if alignment == .FirstBaseline && axis == .Horizontal {
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
if #available(iOS 8, *) {
if alignment == .FirstBaseline && axis == .Horizontal {
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
}
}

if distribution == .FillEqually {
Expand Down Expand Up @@ -257,8 +257,10 @@ public class TZStackView: UIView {
switch axis {
case .Horizontal:
stackViewConstraints.append(constraint(item: self, attribute: .Width, toItem: nil, attribute: .NotAnAttribute, priority: 49))
if alignment == .FirstBaseline {
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
if #available(iOS 8, *) {
if alignment == .FirstBaseline {
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
}
}
case .Vertical:
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
Expand All @@ -283,8 +285,10 @@ public class TZStackView: UIView {
switch axis {
case .Horizontal:
stackViewConstraints.append(constraint(item: self, attribute: .Width, toItem: nil, attribute: .NotAnAttribute, priority: 49))
if alignment == .FirstBaseline {
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
if #available(iOS 8, *) {
if alignment == .FirstBaseline {
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
}
}
case .Vertical:
stackViewConstraints.append(constraint(item: self, attribute: .Height, toItem: nil, attribute: .NotAnAttribute, priority: 49))
Expand Down Expand Up @@ -315,17 +319,25 @@ public class TZStackView: UIView {
stackViewConstraints += createSurroundingSpacerViewConstraints(spacerViews[0], 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 #available(iOS 8, *) {
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]))
}
}
}
addConstraints(stackViewConstraints)
}


let constraintsToActivate = subviewConstraints + stackViewConstraints
if #available(iOS 8.0, *) {
NSLayoutConstraint.activateConstraints(constraintsToActivate)
} else {
addConstraints(constraintsToActivate)
}

super.updateConstraints()
}

Expand Down Expand Up @@ -468,37 +480,50 @@ public class TZStackView: UIView {
private func createMatchEdgesContraints(views: [UIView]) -> [NSLayoutConstraint] {
var constraints = [NSLayoutConstraint]()

switch axis {
case .Horizontal:
switch alignment {
case .Fill:
constraints += equalAttributes(views: views, attribute: .Bottom)
constraints += equalAttributes(views: views, attribute: .Top)
case .Center:
constraints += equalAttributes(views: views, attribute: .CenterY)
case .Leading, .Top:
constraints += equalAttributes(views: views, attribute: .Top)
case .Trailing, .Bottom:
constraints += equalAttributes(views: views, attribute: .Bottom)
case .FirstBaseline:
constraints += equalAttributes(views: views, attribute: .FirstBaseline)
}
switch (alignment, axis) {
case (.Fill, .Horizontal): // Fill alignment
constraints += equalAttributes(views: views, attribute: .Bottom)
constraints += equalAttributes(views: views, attribute: .Top)
case (.Fill, .Vertical):
constraints += equalAttributes(views: views, attribute: .Leading)
constraints += equalAttributes(views: views, attribute: .Trailing)

case .Vertical:
switch alignment {
case .Fill:
constraints += equalAttributes(views: views, attribute: .Leading)
constraints += equalAttributes(views: views, attribute: .Trailing)
case .Center:
constraints += equalAttributes(views: views, attribute: .CenterX)
case .Leading, .Top:
constraints += equalAttributes(views: views, attribute: .Leading)
case .Trailing, .Bottom:
constraints += equalAttributes(views: views, attribute: .Trailing)
case .FirstBaseline:
case (.Center, .Horizontal): // Center alignment
constraints += equalAttributes(views: views, attribute: .CenterY)
case (.Center, .Vertical):
constraints += equalAttributes(views: views, attribute: .CenterX)

case (.Leading, .Horizontal): // Leading & Top alignment
constraints += equalAttributes(views: views, attribute: .Top)
case (.Leading, .Vertical):
constraints += equalAttributes(views: views, attribute: .Leading)

case (.Trailing, .Horizontal): // Trailing and Bottom alignment
constraints += equalAttributes(views: views, attribute: .Bottom)
case (.Trailing, .Vertical):
constraints += equalAttributes(views: views, attribute: .Trailing)

case (.LastBaseline, .Horizontal): // Last-Baseline alignment, works only on horizontal axis
if #available(iOS 8, *) {
constraints += equalAttributes(views: views, attribute: .LastBaseline)
} else {
constraints += equalAttributes(views: views, attribute: .Baseline)
}
case (.LastBaseline, .Vertical):
constraints += []
default: break
}

if #available(iOS 8, *) { // First-Baseline alignment requires iOS 8+
switch (alignment, axis) {
case (.FirstBaseline, .Horizontal): // First-Baseline alignment, works only on horizontal axis
constraints += equalAttributes(views: views, attribute: .FirstBaseline)
case (.FirstBaseline, .Vertical):
constraints += []
default: break
}
}

return constraints
}

Expand All @@ -512,21 +537,26 @@ public class TZStackView: UIView {

var topView = arrangedSubviews.first!
var bottomView = arrangedSubviews.first!

if spacerViews.count > 0 {
if alignment == .Center {
switch (alignment, axis) {
case (.Center, _):
topView = spacerViews[0]
bottomView = spacerViews[0]
} else if alignment == .Top || alignment == .Leading {
case (.Leading, _):
bottomView = spacerViews[0]
} else if alignment == .Bottom || alignment == .Trailing {
case (.Trailing, _):
topView = spacerViews[0]
} else if alignment == .FirstBaseline {
switch axis {
case .Horizontal:
bottomView = spacerViews[0]
case .Vertical:
topView = spacerViews[0]
case (.LastBaseline, .Horizontal):
bottomView = spacerViews[0]
default: break
}

if #available(iOS 8, *) {
switch (alignment, axis) {
case (.FirstBaseline, .Horizontal):
bottomView = spacerViews[0]
default: break
}
}
}
Expand Down
33 changes: 29 additions & 4 deletions TZStackView/TZStackViewAlignment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,36 @@
import Foundation

@objc public enum TZStackViewAlignment: Int {
/* Align the leading and trailing edges of vertically stacked items
or the top and bottom edges of horizontally stacked items tightly to the container.
*/
case Fill
case Center

/* Align the leading edges of vertically stacked items
or the top edges of horizontally stacked items tightly to the relevant edge
of the container
*/
case Leading
case Top
public static var Top: TZStackViewAlignment {
get {
return .Leading
}
}
case FirstBaseline // Valid for horizontal axis only

/* Center the items in a vertical stack horizontally
or the items in a horizontal stack vertically
*/
case Center

/* Align the trailing edges of vertically stacked items
or the bottom edges of horizontally stacked items tightly to the relevant
edge of the container
*/
case Trailing
case Bottom
case FirstBaseline
public static var Bottom: TZStackViewAlignment { get {
return .Trailing
}
}
case LastBaseline // Valid for horizontal axis only
}
6 changes: 4 additions & 2 deletions TZStackViewDemo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class ViewController: UIViewController {
axisSegmentedControl.setContentCompressionResistancePriority(900, forAxis: .Horizontal)
axisSegmentedControl.tintColor = UIColor.lightGrayColor()

alignmentSegmentedControl = UISegmentedControl(items: ["Fill", "Center", "Leading", "Top", "Trailing", "Bottom", "FirstBaseline"])
alignmentSegmentedControl = UISegmentedControl(items: ["Fill", "Center", "Leading", "Top", "Trailing", "Bottom", "FirstBaseline", "LastBaseline"])
alignmentSegmentedControl.selectedSegmentIndex = 0
alignmentSegmentedControl.addTarget(self, action: "alignmentChanged:", forControlEvents: .ValueChanged)
alignmentSegmentedControl.setContentCompressionResistancePriority(1000, forAxis: .Horizontal)
Expand Down Expand Up @@ -156,8 +156,10 @@ class ViewController: UIViewController {
tzStackView.alignment = .Trailing
case 5:
tzStackView.alignment = .Bottom
default:
case 6:
tzStackView.alignment = .FirstBaseline
default:
tzStackView.alignment = .LastBaseline
}
tzStackView.setNeedsUpdateConstraints()
}
Expand Down