diff --git a/TZStackView.xcodeproj/project.pbxproj b/TZStackView.xcodeproj/project.pbxproj index b9e35a1..7be90c9 100644 --- a/TZStackView.xcodeproj/project.pbxproj +++ b/TZStackView.xcodeproj/project.pbxproj @@ -13,13 +13,14 @@ 5F50E9F2F7E5B2DA68C946E0 /* ExplicitIntrinsicContentSizeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F50E5EB8202F5247F8517F3 /* ExplicitIntrinsicContentSizeView.swift */; }; 5F50EAD959E8ACC5929DBD75 /* NSLayoutConstraintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */; }; 5F50EF474D670FC33E8E80EA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F50ED5A43FBFC32B9B9E1AA /* Images.xcassets */; }; + 7E214B741BF61501006448AC /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7E214B731BF61501006448AC /* Storyboard.storyboard */; }; 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 */; }; @@ -67,6 +68,7 @@ 5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TZStackViewTests.swift; sourceTree = ""; }; 5F50EF54F01A3A6938C6CEA1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 5F50EFD0C46B7C7F989F10E1 /* TZStackViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TZStackViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7E214B731BF61501006448AC /* Storyboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Storyboard.storyboard; sourceTree = ""; }; A45441BF1B9B6D71002452BA /* TZStackView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TZStackView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A45441C11B9B6D71002452BA /* TZStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TZStackView.h; sourceTree = ""; }; A45441C31B9B6D71002452BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -152,6 +154,7 @@ 5F50ECF2FC09D64EB02F1309 /* ViewController.swift */, 5F50E5EB8202F5247F8517F3 /* ExplicitIntrinsicContentSizeView.swift */, DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */, + 7E214B731BF61501006448AC /* Storyboard.storyboard */, DB614E5D1B292B630024E8AD /* LaunchScreen.xib */, 5F50ECF4A13687970498D9A7 /* Supporting Files */, 5F50ED5A43FBFC32B9B9E1AA /* Images.xcassets */, @@ -298,6 +301,7 @@ files = ( DB614E5E1B292B630024E8AD /* LaunchScreen.xib in Resources */, 5F50EF474D670FC33E8E80EA /* Images.xcassets in Resources */, + 7E214B741BF61501006448AC /* Storyboard.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TZStackView/TZStackView.h b/TZStackView/TZStackView.h old mode 100644 new mode 100755 index 1659c1c..3e79ff7 --- a/TZStackView/TZStackView.h +++ b/TZStackView/TZStackView.h @@ -16,4 +16,5 @@ FOUNDATION_EXPORT const unsigned char TZStackViewVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import - +#define TZStackViewAlignmentTop TZStackViewAlignmentLeading +#define TZStackViewAlignmentBottom TZStackViewAlignmentTrailing diff --git a/TZStackView/TZStackView.swift b/TZStackView/TZStackView.swift index 2fd77a8..c0fbfc4 100755 --- a/TZStackView/TZStackView.swift +++ b/TZStackView/TZStackView.swift @@ -17,6 +17,7 @@ func ==(lhs: TZAnimationDidStopQueueEntry, rhs: TZAnimationDidStopQueueEntry) -> return lhs.view === rhs.view } +@IBDesignable public class TZStackView: UIView { public var distribution: TZStackViewDistribution = .Fill { @@ -32,10 +33,37 @@ public class TZStackView: UIView { } public var alignment: TZStackViewAlignment = .Fill + + @objc @IBInspectable private var axisValue: Int { + get { + return axis.rawValue + } + set { + axis = UILayoutConstraintAxis(rawValue: newValue) ?? .Horizontal + } + } + + @objc @IBInspectable private var alignmentValue: Int { + get { + return alignment.rawValue + } + set { + alignment = TZStackViewAlignment(rawValue: newValue) ?? .Fill + } + } + + @objc @IBInspectable private var distributionValue: Int { + get { + return distribution.rawValue + } + set { + distribution = TZStackViewDistribution(rawValue: newValue) ?? .Fill + } + } - public var spacing: CGFloat = 0 + @objc @IBInspectable public var spacing: CGFloat = 0 - public var layoutMarginsRelativeArrangement = false + @objc @IBInspectable public var layoutMarginsRelativeArrangement: Bool = false public private(set) var arrangedSubviews: [UIView] = [] { didSet { @@ -59,6 +87,20 @@ public class TZStackView: UIView { public init(arrangedSubviews: [UIView] = []) { super.init(frame: CGRectZero) + commonInit(arrangedSubviews) + } + + override public init(frame: CGRect) { + super.init(frame: frame) + commonInit([]) + } + + required public init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder)! + commonInit([]) + } + + private func commonInit(arrangedSubviews: [UIView]) { for arrangedSubview in arrangedSubviews { arrangedSubview.translatesAutoresizingMaskIntoConstraints = false addSubview(arrangedSubview) @@ -72,7 +114,30 @@ public class TZStackView: UIView { // This removes `hidden` value KVO observers using didSet() { self.arrangedSubviews = [] }() } + + override public func awakeFromNib() { + super.awakeFromNib() + + // a hack, remove Interface Builder generated constraints that are not created by you + for aConstraint in constraints where NSStringFromClass(aConstraint.dynamicType) == "NSIBPrototypingLayoutConstraint" { + removeConstraint(aConstraint) + } + + for aView in subviews { + addArrangedSubview(aView) + } + } + override public func prepareForInterfaceBuilder() { + if #available(iOS 8.0, *) { + super.prepareForInterfaceBuilder() + + for aView in subviews { + addArrangedSubview(aView) + } + } + } + private func registerHiddenListeners(previousArrangedSubviews: [UIView]) { for subview in previousArrangedSubviews { self.removeHiddenListener(subview) @@ -329,10 +394,6 @@ public class TZStackView: UIView { super.updateConstraints() } - required public init(coder aDecoder: NSCoder) { - super.init(coder: aDecoder)! - } - private func addSpacerView() -> TZSpacerView { let spacerView = TZSpacerView() spacerView.translatesAutoresizingMaskIntoConstraints = false @@ -476,9 +537,9 @@ public class TZStackView: UIView { constraints += equalAttributes(views: views, attribute: .Top) case .Center: constraints += equalAttributes(views: views, attribute: .CenterY) - case .Leading, .Top: + case .Leading: constraints += equalAttributes(views: views, attribute: .Top) - case .Trailing, .Bottom: + case .Trailing: constraints += equalAttributes(views: views, attribute: .Bottom) case .FirstBaseline: constraints += equalAttributes(views: views, attribute: .FirstBaseline) @@ -491,9 +552,9 @@ public class TZStackView: UIView { constraints += equalAttributes(views: views, attribute: .Trailing) case .Center: constraints += equalAttributes(views: views, attribute: .CenterX) - case .Leading, .Top: + case .Leading: constraints += equalAttributes(views: views, attribute: .Leading) - case .Trailing, .Bottom: + case .Trailing: constraints += equalAttributes(views: views, attribute: .Trailing) case .FirstBaseline: constraints += [] diff --git a/TZStackView/TZStackViewAlignment.swift b/TZStackView/TZStackViewAlignment.swift old mode 100644 new mode 100755 index f99594f..235d83f --- a/TZStackView/TZStackViewAlignment.swift +++ b/TZStackView/TZStackViewAlignment.swift @@ -8,12 +8,33 @@ import Foundation -@objc public enum TZStackViewAlignment: Int { - case Fill - case Center - case Leading - case Top - case Trailing - case Bottom - case FirstBaseline +/* Alignment—the layout transverse to the stacking axis. +*/ +@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 = 0 + + /* 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 = 1 + public static let Top: TZStackViewAlignment = .Leading + case FirstBaseline = 2 // Valid for horizontal axis only + + /* Center the items in a vertical stack horizontally + or the items in a horizontal stack vertically + */ + case Center = 3 + + /* 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 = 4 + public static let Bottom: TZStackViewAlignment = .Trailing +// case LastBaseline = 5 // Valid for horizontal axis only } diff --git a/TZStackView/TZStackViewDistribution.swift b/TZStackView/TZStackViewDistribution.swift old mode 100644 new mode 100755 index 0802276..9a65231 --- a/TZStackView/TZStackViewDistribution.swift +++ b/TZStackView/TZStackViewDistribution.swift @@ -7,10 +7,45 @@ // import Foundation -@objc public enum TZStackViewDistribution: Int { - case Fill - case FillEqually - case FillProportionally - case EqualSpacing - case EqualCentering +/* Distribution—the layout along the stacking axis. + +All UIStackViewDistribution enum values fit first and last arranged subviews tightly to the container, +and except for UIStackViewDistributionFillEqually, fit all items to intrinsicContentSize when possible. +*/ +@objc public enum TZStackViewDistribution : Int { + + /* When items do not fit (overflow) or fill (underflow) the space available + adjustments occur according to compressionResistance or hugging + priorities of items, or when that is ambiguous, according to arrangement + order. + */ + case Fill = 0 + + /* Items are all the same size. + When space allows, this will be the size of the item with the largest + intrinsicContentSize (along the axis of the stack). + Overflow or underflow adjustments are distributed equally among the items. + */ + case FillEqually = 1 + + /* Overflow or underflow adjustments are distributed among the items proportional + to their intrinsicContentSizes. + */ + case FillProportionally = 2 + + /* Additional underflow spacing is divided equally in the spaces between the items. + Overflow squeezing is controlled by compressionResistance priorities followed by + arrangement order. + */ + case EqualSpacing = 3 + + /* Equal center-to-center spacing of the items is maintained as much + as possible while still maintaining a minimum edge-to-edge spacing within the + allowed area. + Additional underflow spacing is divided equally in the spacing. Overflow + squeezing is distributed first according to compressionResistance priorities + of items, then according to subview order while maintaining the configured + (edge-to-edge) spacing as a minimum. + */ + case EqualCentering = 4 } diff --git a/TZStackViewDemo/Storyboard.storyboard b/TZStackViewDemo/Storyboard.storyboard new file mode 100644 index 0000000..4cf23a6 --- /dev/null +++ b/TZStackViewDemo/Storyboard.storyboard @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TZStackViewDemo/ViewController.swift b/TZStackViewDemo/ViewController.swift index 482cd17..076fa29 100644 --- a/TZStackViewDemo/ViewController.swift +++ b/TZStackViewDemo/ViewController.swift @@ -39,12 +39,21 @@ class ViewController: UIViewController { tzStackView.spacing = 15 view.addSubview(tzStackView) + let toIBButton = UIButton(type: .System) + toIBButton.translatesAutoresizingMaskIntoConstraints = false + toIBButton.setTitle("Storyboard Views", forState: .Normal) + toIBButton.addTarget(self, action: "toIB", forControlEvents: .TouchUpInside) + toIBButton.setContentCompressionResistancePriority(1000, forAxis: .Vertical) + toIBButton.setContentHuggingPriority(1000, forAxis: .Vertical) + view.addSubview(toIBButton) + instructionLabel.translatesAutoresizingMaskIntoConstraints = false instructionLabel.font = UIFont.systemFontOfSize(15) instructionLabel.text = "Tap any of the boxes to set hidden=true" instructionLabel.textColor = UIColor.whiteColor() instructionLabel.numberOfLines = 0 instructionLabel.setContentCompressionResistancePriority(900, forAxis: .Horizontal) + instructionLabel.setContentCompressionResistancePriority(1000, forAxis: .Vertical) instructionLabel.setContentHuggingPriority(1000, forAxis: .Vertical) view.addSubview(instructionLabel) @@ -53,34 +62,37 @@ class ViewController: UIViewController { resetButton.addTarget(self, action: "reset", forControlEvents: .TouchUpInside) resetButton.setContentCompressionResistancePriority(1000, forAxis: .Horizontal) resetButton.setContentHuggingPriority(1000, forAxis: .Horizontal) - resetButton.setContentHuggingPriority(1000, forAxis: .Vertical) view.addSubview(resetButton) axisSegmentedControl = UISegmentedControl(items: ["Vertical", "Horizontal"]) axisSegmentedControl.selectedSegmentIndex = 0 axisSegmentedControl.addTarget(self, action: "axisChanged:", forControlEvents: .ValueChanged) - axisSegmentedControl.setContentCompressionResistancePriority(900, forAxis: .Horizontal) + axisSegmentedControl.setContentCompressionResistancePriority(1000, forAxis: .Vertical) + axisSegmentedControl.setContentHuggingPriority(1000, forAxis: .Vertical) axisSegmentedControl.tintColor = UIColor.lightGrayColor() alignmentSegmentedControl = UISegmentedControl(items: ["Fill", "Center", "Leading", "Top", "Trailing", "Bottom", "FirstBaseline"]) alignmentSegmentedControl.selectedSegmentIndex = 0 alignmentSegmentedControl.addTarget(self, action: "alignmentChanged:", forControlEvents: .ValueChanged) - alignmentSegmentedControl.setContentCompressionResistancePriority(1000, forAxis: .Horizontal) + alignmentSegmentedControl.setContentCompressionResistancePriority(1000, forAxis: .Vertical) + alignmentSegmentedControl.setContentHuggingPriority(1000, forAxis: .Vertical) alignmentSegmentedControl.tintColor = UIColor.lightGrayColor() distributionSegmentedControl = UISegmentedControl(items: ["Fill", "FillEqually", "FillProportionally", "EqualSpacing", "EqualCentering"]) distributionSegmentedControl.selectedSegmentIndex = 0 distributionSegmentedControl.addTarget(self, action: "distributionChanged:", forControlEvents: .ValueChanged) + distributionSegmentedControl.setContentCompressionResistancePriority(1000, forAxis: .Vertical) + distributionSegmentedControl.setContentHuggingPriority(1000, forAxis: .Vertical) distributionSegmentedControl.tintColor = UIColor.lightGrayColor() let controlsLayoutContainer = TZStackView(arrangedSubviews: [axisSegmentedControl, alignmentSegmentedControl, distributionSegmentedControl]) controlsLayoutContainer.axis = .Vertical controlsLayoutContainer.translatesAutoresizingMaskIntoConstraints = false controlsLayoutContainer.spacing = 5 - controlsLayoutContainer.setContentHuggingPriority(1000, forAxis: .Vertical) view.addSubview(controlsLayoutContainer) let views: [String:AnyObject] = [ + "toIBButton": toIBButton, "instructionLabel": instructionLabel, "resetButton": resetButton, "tzStackView": tzStackView, @@ -92,6 +104,8 @@ class ViewController: UIViewController { "topspacing": 25 ] + view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[toIBButton]-gap-|", + options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-gap-[instructionLabel]-[resetButton]-gap-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[tzStackView]|", @@ -99,10 +113,9 @@ class ViewController: UIViewController { view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[controlsLayoutContainer]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-topspacing-[instructionLabel]-gap-[controlsLayoutContainer]-gap-[tzStackView]|", - options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-topspacing-[resetButton]-gap-[controlsLayoutContainer]", + view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-topspacing-[toIBButton]-gap-[instructionLabel]-gap-[controlsLayoutContainer]-gap-[tzStackView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) + view.addConstraint(NSLayoutConstraint(item: instructionLabel, attribute: .CenterY, relatedBy: .Equal, toItem: resetButton, attribute: .CenterY, multiplier: 1, constant: 0)) } private func createViews() -> [UIView] { @@ -111,6 +124,16 @@ class ViewController: UIViewController { let blueView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 60, height: 60), name: "Blue") let purpleView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 80, height: 80), name: "Purple") let yellowView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 100, height: 100), name: "Yellow") + + let views = [redView, greenView, blueView, purpleView, yellowView] + + for (i, view) in views.enumerate() { + let j = UILayoutPriority(i) + view.setContentCompressionResistancePriority(751 + j, forAxis: .Horizontal) + view.setContentCompressionResistancePriority(751 + j, forAxis: .Vertical) + view.setContentHuggingPriority(251 + j, forAxis: .Horizontal) + view.setContentHuggingPriority(251 + j, forAxis: .Vertical) + } redView.backgroundColor = UIColor.redColor().colorWithAlphaComponent(0.75) greenView.backgroundColor = UIColor.greenColor().colorWithAlphaComponent(0.75) @@ -130,6 +153,22 @@ class ViewController: UIViewController { }, completion: nil) } + + func toIB() { + + let storyboard = UIStoryboard(name: "Storyboard", bundle: nil) + let viewController = storyboard.instantiateInitialViewController()! + viewController.navigationItem.rightBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: .Cancel, target: self, action: "backFromIB") + + let navController = UINavigationController(rootViewController: viewController) + + presentViewController(navController, animated: true, completion: nil) + + } + + func backFromIB() { + dismissViewControllerAnimated(true, completion:nil) + } //MARK: - Segmented Control Actions //--------------------------------------------------------------------------