Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TZStackViewAlignment compatibility and better iOS 7 support #63

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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