├── .gitignore ├── .swift-version ├── LICENSE ├── README.md ├── TZStackView.podspec ├── TZStackView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── TZStackView.xcscheme ├── TZStackView ├── Info.plist ├── TZSpacerView.swift ├── TZStackView.h ├── TZStackView.swift ├── TZStackViewAlignment.swift └── TZStackViewDistribution.swift ├── TZStackViewDemo ├── AppDelegate.swift ├── ExplicitIntrinsicContentSizeView.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── LaunchScreen.xib ├── NSLayoutConstraintExtension.swift └── ViewController.swift ├── TZStackViewTests ├── Info.plist ├── TZStackViewTestCase.swift ├── TZStackViewTests.swift └── TestView.swift └── assets ├── TZStackView-hide-animation.gif └── layout-example.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | # Pods/ 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | 33 | Carthage/Build 34 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tom van Zummeren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TZStackView [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![Swift 3.0.x](https://img.shields.io/badge/Swift-3.0.x-orange.svg) 2 | A wonderful layout component called the [`UIStackView` was introduced with *iOS 9*](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/). With this component it is really easy to layout components in a row both horizontally and vertically. Apple recommends using the `UIStackView` wherever possible and resort to explicit `NSLayoutConstraints` only when there is no way to do it with `UIStackView`. This saves you lots of boiler plate `NSLayoutConstraint` creation code. 3 | 4 | `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`. 5 | 6 | ## Features 7 | - ✅ Compatible with **iOS 7.x** and **iOS 8.x** 8 | - ✅ Supports the complete API of `UIStackView` including **all** *distribution* and *alignment* options 9 | - ✅ Supports animating the `hidden` property of the *arranged subviews* 10 | - ❌ Supports *Storyboard* 11 | 12 | So this implementation does **not** support Storyboard. It is meant for iOS developers who, like me, want to use the `UIStackView` in our existing apps and like to layout their components in code as opposed to using *Storyboard*. 13 | 14 | ## Setup 15 | You basically have two options to include the `TZStackView` in your *Xcode* project: 16 | 17 | ### Use [CocoaPods](http://cocoapods.org/) 18 | Example `Podfile`: 19 | ```ruby 20 | source 'https://github.com/CocoaPods/Specs.git' 21 | platform :ios, "8.0" 22 | use_frameworks! 23 | 24 | pod "TZStackView", "1.2.0" 25 | ``` 26 | Unfortunately, using CocoaPods with a Swift pod requires iOS 8. 27 | 28 | ### Use [Carthage](https://github.com/Carthage/Carthage) 29 | 30 | Example `Cartfile`: 31 | ``` 32 | github "tomvanzummeren/TZStackView" ~> 1.2.0 33 | ``` 34 | 35 | Run `carthage` to build the framework and drag the built `TZStackView.framework` into your Xcode project. 36 | 37 | ### Drag files directly into your project 38 | Alternatively (when you do want to support iOS 7) drag in the following classes from the *Example* folder directly into your *Xcode* project 39 | * `TZStackView` 40 | * `TZSpacerView` 41 | * `TZStackViewAlignment` 42 | * `TZStackViewDistribution` 43 | 44 | ## Example usage 45 | Given `view1`, `view2` and `view3` who have intrinsic content sizes set to *100x100*, *80x80* and *60x60* respectively. 46 | 47 | ```swift 48 | let stackView = TZStackView(arrangedSubviews: [view1, view2, view3]) 49 | stackView.distribution = .FillEqually 50 | stackView.alignment = .Center 51 | stackView.axis = .Vertical 52 | stackView.spacing = 25 53 | ``` 54 | 55 | This would produce the following layout: 56 | 57 | ![TZStackView Layout example](/assets/layout-example.png) 58 | 59 | See the [developer documentation for `UIStackView`](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/) for all other combinatins of distributions, alignments and axis. `TZStackView` works and behaves exactly the same way as the `UIStackView` except for not supporting Storyboard. If you do find a case where it does not behave the same way, please file a bug report. 60 | 61 | To animate adding a view to or removing a view from the arranged subviews, simply hide or show them by adjusting the `hidden` property within an animation block (as described by the `UIStackView` reference docs as well): 62 | 63 | ```swift 64 | UIView.animateWithDuration(0.5, animations: { 65 | self.view2.hidden = true 66 | }) 67 | ``` 68 | ![TZStackView hidden animation example](/assets/TZStackView-hide-animation.gif) 69 | 70 | ## Migrating to UIStackView 71 | If at a later point you decide to make *iOS 9* the minimum requirement of your app (it will happen sooner or later), you will want to migrate to the real `UIStackView` instead of using this implementation. Because the `TZStackView` is a drop-in replacement for `UIStackView`, you simply replace: 72 | 73 | ```swift 74 | let stackView = TZStackView(arrangedSubviews: views) 75 | ``` 76 | 77 | with ... 78 | 79 | ```swift 80 | let stackView = UIStackView(arrangedSubviews: views) 81 | ``` 82 | 83 | ... and you're good to go! You will not need to make any other changes and everything will simply work the way it worked before. 84 | 85 | ## License 86 | TZStackView is released under the MIT license. See LICENSE for details. 87 | -------------------------------------------------------------------------------- /TZStackView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "TZStackView" 3 | s.version = "1.3.0" 4 | s.summary = "TZStackView is a replica of iOS 9's new UIStackView for use in iOS 7 and iOS 8" 5 | s.homepage = "https://github.com/tomvanzummeren/TZStackView.git" 6 | s.license = { :type => "MIT" } 7 | s.authors = { "tomvanzummeren" => "tom.van.zummeren@gmail.com" } 8 | 9 | s.requires_arc = true 10 | s.ios.deployment_target = "8.0" 11 | s.source = { :git => "https://github.com/tomvanzummeren/TZStackView.git", :tag => "1.3.0"} 12 | s.source_files = "TZStackView/*.swift" 13 | end 14 | -------------------------------------------------------------------------------- /TZStackView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5F50E6EAC8DDE44079E03638 /* TZStackViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */; }; 11 | 5F50E81C644B9C88791038B0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F50EF54F01A3A6938C6CEA1 /* AppDelegate.swift */; }; 12 | 5F50E83B164FAE28B52A5A91 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F50ECF2FC09D64EB02F1309 /* ViewController.swift */; }; 13 | 5F50E9F2F7E5B2DA68C946E0 /* ExplicitIntrinsicContentSizeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F50E5EB8202F5247F8517F3 /* ExplicitIntrinsicContentSizeView.swift */; }; 14 | 5F50EAD959E8ACC5929DBD75 /* NSLayoutConstraintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */; }; 15 | 5F50EF474D670FC33E8E80EA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F50ED5A43FBFC32B9B9E1AA /* Images.xcassets */; }; 16 | A45441C21B9B6D71002452BA /* TZStackView.h in Headers */ = {isa = PBXBuildFile; fileRef = A45441C11B9B6D71002452BA /* TZStackView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | A45441C61B9B6D71002452BA /* TZStackView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A45441BF1B9B6D71002452BA /* TZStackView.framework */; }; 18 | A45441C71B9B6D71002452BA /* TZStackView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A45441BF1B9B6D71002452BA /* TZStackView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 19 | A45441D01B9B6D9C002452BA /* TZSpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CC1B9B6D9C002452BA /* TZSpacerView.swift */; }; 20 | A45441D11B9B6D9C002452BA /* TZStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CD1B9B6D9C002452BA /* TZStackView.swift */; }; 21 | A45441D21B9B6D9C002452BA /* TZStackViewAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */; }; 22 | A45441D31B9B6D9C002452BA /* TZStackViewDistribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */; }; 23 | DB41AF6A1B294B8E003DB902 /* NSLayoutConstraintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */; }; 24 | DB5B70851B2A1963006043BD /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B70841B2A1963006043BD /* TestView.swift */; }; 25 | DB5B70871B2B8816006043BD /* TZStackViewTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B70861B2B8816006043BD /* TZStackViewTestCase.swift */; }; 26 | DB614E5E1B292B630024E8AD /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = DB614E5D1B292B630024E8AD /* LaunchScreen.xib */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | 5F50E2D085856B752DDB3B93 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 5F50E8956739A06BEE120598 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 5F50E36345E94CFC65ED7270; 35 | remoteInfo = TZStackView; 36 | }; 37 | A45441C41B9B6D71002452BA /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 5F50E8956739A06BEE120598 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = A45441BE1B9B6D71002452BA; 42 | remoteInfo = TZStackView; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXCopyFilesBuildPhase section */ 47 | A45441CB1B9B6D71002452BA /* Embed Frameworks */ = { 48 | isa = PBXCopyFilesBuildPhase; 49 | buildActionMask = 2147483647; 50 | dstPath = ""; 51 | dstSubfolderSpec = 10; 52 | files = ( 53 | A45441C71B9B6D71002452BA /* TZStackView.framework in Embed Frameworks */, 54 | ); 55 | name = "Embed Frameworks"; 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXCopyFilesBuildPhase section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | 5F50E1340FBDFB7D123C9204 /* TZStackViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TZStackViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 5F50E5EB8202F5247F8517F3 /* ExplicitIntrinsicContentSizeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExplicitIntrinsicContentSizeView.swift; sourceTree = ""; }; 63 | 5F50E9955790F6814D494857 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; 64 | 5F50ECE89FF2DA8F6B16DB3E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; 65 | 5F50ECF2FC09D64EB02F1309 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 66 | 5F50ED5A43FBFC32B9B9E1AA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 67 | 5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TZStackViewTests.swift; sourceTree = ""; }; 68 | 5F50EF54F01A3A6938C6CEA1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 69 | 5F50EFD0C46B7C7F989F10E1 /* TZStackViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TZStackViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | A45441BF1B9B6D71002452BA /* TZStackView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TZStackView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | A45441C11B9B6D71002452BA /* TZStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TZStackView.h; sourceTree = ""; }; 72 | A45441C31B9B6D71002452BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | A45441CC1B9B6D9C002452BA /* TZSpacerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TZSpacerView.swift; sourceTree = ""; }; 74 | A45441CD1B9B6D9C002452BA /* TZStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TZStackView.swift; sourceTree = ""; }; 75 | A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TZStackViewAlignment.swift; sourceTree = ""; }; 76 | A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TZStackViewDistribution.swift; sourceTree = ""; }; 77 | DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraintExtension.swift; sourceTree = ""; }; 78 | DB5B70841B2A1963006043BD /* TestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = ""; }; 79 | DB5B70861B2B8816006043BD /* TZStackViewTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TZStackViewTestCase.swift; sourceTree = ""; }; 80 | DB614E5D1B292B630024E8AD /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = ""; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | 5F50E714DA218369BD219AE0 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 5F50EFCE1AB77614030E1791 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | A45441C61B9B6D71002452BA /* TZStackView.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | A45441BB1B9B6D71002452BA /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | 5F50E05A91CC731E5AFD4E94 /* Supporting Files */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 5F50E9955790F6814D494857 /* Info.plist */, 113 | ); 114 | name = "Supporting Files"; 115 | sourceTree = ""; 116 | }; 117 | 5F50E6B23A7FD45BDBD44C86 = { 118 | isa = PBXGroup; 119 | children = ( 120 | A45441C01B9B6D71002452BA /* TZStackView */, 121 | 5F50E7526ADB7151E0540D2D /* TZStackViewTests */, 122 | 5F50EC0CBAE86A71ABF0B3BA /* TZStackViewDemo */, 123 | 5F50EB61965EE1FD3F76FB91 /* Products */, 124 | ); 125 | sourceTree = ""; 126 | }; 127 | 5F50E7526ADB7151E0540D2D /* TZStackViewTests */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */, 131 | DB5B70841B2A1963006043BD /* TestView.swift */, 132 | DB5B70861B2B8816006043BD /* TZStackViewTestCase.swift */, 133 | 5F50E05A91CC731E5AFD4E94 /* Supporting Files */, 134 | ); 135 | path = TZStackViewTests; 136 | sourceTree = ""; 137 | }; 138 | 5F50EB61965EE1FD3F76FB91 /* Products */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 5F50E1340FBDFB7D123C9204 /* TZStackViewDemo.app */, 142 | 5F50EFD0C46B7C7F989F10E1 /* TZStackViewTests.xctest */, 143 | A45441BF1B9B6D71002452BA /* TZStackView.framework */, 144 | ); 145 | name = Products; 146 | sourceTree = ""; 147 | }; 148 | 5F50EC0CBAE86A71ABF0B3BA /* TZStackViewDemo */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 5F50EF54F01A3A6938C6CEA1 /* AppDelegate.swift */, 152 | 5F50ECF2FC09D64EB02F1309 /* ViewController.swift */, 153 | 5F50E5EB8202F5247F8517F3 /* ExplicitIntrinsicContentSizeView.swift */, 154 | DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */, 155 | DB614E5D1B292B630024E8AD /* LaunchScreen.xib */, 156 | 5F50ECF4A13687970498D9A7 /* Supporting Files */, 157 | 5F50ED5A43FBFC32B9B9E1AA /* Images.xcassets */, 158 | ); 159 | path = TZStackViewDemo; 160 | sourceTree = ""; 161 | }; 162 | 5F50ECF4A13687970498D9A7 /* Supporting Files */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 5F50ECE89FF2DA8F6B16DB3E /* Info.plist */, 166 | ); 167 | name = "Supporting Files"; 168 | sourceTree = ""; 169 | }; 170 | A45441C01B9B6D71002452BA /* TZStackView */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | A45441CC1B9B6D9C002452BA /* TZSpacerView.swift */, 174 | A45441CD1B9B6D9C002452BA /* TZStackView.swift */, 175 | A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */, 176 | A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */, 177 | A45441D41B9B6E46002452BA /* Supporting Files */, 178 | ); 179 | path = TZStackView; 180 | sourceTree = ""; 181 | }; 182 | A45441D41B9B6E46002452BA /* Supporting Files */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | A45441C11B9B6D71002452BA /* TZStackView.h */, 186 | A45441C31B9B6D71002452BA /* Info.plist */, 187 | ); 188 | name = "Supporting Files"; 189 | sourceTree = ""; 190 | }; 191 | /* End PBXGroup section */ 192 | 193 | /* Begin PBXHeadersBuildPhase section */ 194 | A45441BC1B9B6D71002452BA /* Headers */ = { 195 | isa = PBXHeadersBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | A45441C21B9B6D71002452BA /* TZStackView.h in Headers */, 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | }; 202 | /* End PBXHeadersBuildPhase section */ 203 | 204 | /* Begin PBXNativeTarget section */ 205 | 5F50E36345E94CFC65ED7270 /* TZStackViewDemo */ = { 206 | isa = PBXNativeTarget; 207 | buildConfigurationList = 5F50EB67250401C8180503F9 /* Build configuration list for PBXNativeTarget "TZStackViewDemo" */; 208 | buildPhases = ( 209 | 5F50EACD0CA47C3C2D5ECE2D /* Sources */, 210 | 5F50EFCE1AB77614030E1791 /* Frameworks */, 211 | 5F50EB90676857A21F608692 /* Resources */, 212 | A45441CB1B9B6D71002452BA /* Embed Frameworks */, 213 | ); 214 | buildRules = ( 215 | ); 216 | dependencies = ( 217 | A45441C51B9B6D71002452BA /* PBXTargetDependency */, 218 | ); 219 | name = TZStackViewDemo; 220 | productName = TZStackView; 221 | productReference = 5F50E1340FBDFB7D123C9204 /* TZStackViewDemo.app */; 222 | productType = "com.apple.product-type.application"; 223 | }; 224 | 5F50EF63134E65D95D5312EE /* TZStackViewTests */ = { 225 | isa = PBXNativeTarget; 226 | buildConfigurationList = 5F50E4200B3FF23FD89C2CE5 /* Build configuration list for PBXNativeTarget "TZStackViewTests" */; 227 | buildPhases = ( 228 | 5F50E06BB91A5156164D83ED /* Sources */, 229 | 5F50E714DA218369BD219AE0 /* Frameworks */, 230 | 5F50EBBEBB53C6116897915E /* Resources */, 231 | ); 232 | buildRules = ( 233 | ); 234 | dependencies = ( 235 | 5F50E9A4787A8017DFBA09FB /* PBXTargetDependency */, 236 | ); 237 | name = TZStackViewTests; 238 | productName = TZStackViewTests; 239 | productReference = 5F50EFD0C46B7C7F989F10E1 /* TZStackViewTests.xctest */; 240 | productType = "com.apple.product-type.bundle.unit-test"; 241 | }; 242 | A45441BE1B9B6D71002452BA /* TZStackView */ = { 243 | isa = PBXNativeTarget; 244 | buildConfigurationList = A45441C81B9B6D71002452BA /* Build configuration list for PBXNativeTarget "TZStackView" */; 245 | buildPhases = ( 246 | A45441BA1B9B6D71002452BA /* Sources */, 247 | A45441BB1B9B6D71002452BA /* Frameworks */, 248 | A45441BC1B9B6D71002452BA /* Headers */, 249 | A45441BD1B9B6D71002452BA /* Resources */, 250 | ); 251 | buildRules = ( 252 | ); 253 | dependencies = ( 254 | ); 255 | name = TZStackView; 256 | productName = TZStackView; 257 | productReference = A45441BF1B9B6D71002452BA /* TZStackView.framework */; 258 | productType = "com.apple.product-type.framework"; 259 | }; 260 | /* End PBXNativeTarget section */ 261 | 262 | /* Begin PBXProject section */ 263 | 5F50E8956739A06BEE120598 /* Project object */ = { 264 | isa = PBXProject; 265 | attributes = { 266 | LastSwiftUpdateCheck = 0700; 267 | LastUpgradeCheck = 0800; 268 | ORGANIZATIONNAME = "Tom van Zummeren"; 269 | TargetAttributes = { 270 | 5F50E36345E94CFC65ED7270 = { 271 | LastSwiftMigration = 0800; 272 | }; 273 | 5F50EF63134E65D95D5312EE = { 274 | LastSwiftMigration = 0800; 275 | }; 276 | A45441BE1B9B6D71002452BA = { 277 | CreatedOnToolsVersion = 7.0; 278 | }; 279 | }; 280 | }; 281 | buildConfigurationList = 5F50EBF385F683E8C65E5F50 /* Build configuration list for PBXProject "TZStackView" */; 282 | compatibilityVersion = "Xcode 3.2"; 283 | developmentRegion = English; 284 | hasScannedForEncodings = 0; 285 | knownRegions = ( 286 | en, 287 | ); 288 | mainGroup = 5F50E6B23A7FD45BDBD44C86; 289 | productRefGroup = 5F50EB61965EE1FD3F76FB91 /* Products */; 290 | projectDirPath = ""; 291 | projectRoot = ""; 292 | targets = ( 293 | A45441BE1B9B6D71002452BA /* TZStackView */, 294 | 5F50E36345E94CFC65ED7270 /* TZStackViewDemo */, 295 | 5F50EF63134E65D95D5312EE /* TZStackViewTests */, 296 | ); 297 | }; 298 | /* End PBXProject section */ 299 | 300 | /* Begin PBXResourcesBuildPhase section */ 301 | 5F50EB90676857A21F608692 /* Resources */ = { 302 | isa = PBXResourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | DB614E5E1B292B630024E8AD /* LaunchScreen.xib in Resources */, 306 | 5F50EF474D670FC33E8E80EA /* Images.xcassets in Resources */, 307 | ); 308 | runOnlyForDeploymentPostprocessing = 0; 309 | }; 310 | 5F50EBBEBB53C6116897915E /* Resources */ = { 311 | isa = PBXResourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | }; 317 | A45441BD1B9B6D71002452BA /* Resources */ = { 318 | isa = PBXResourcesBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | ); 322 | runOnlyForDeploymentPostprocessing = 0; 323 | }; 324 | /* End PBXResourcesBuildPhase section */ 325 | 326 | /* Begin PBXSourcesBuildPhase section */ 327 | 5F50E06BB91A5156164D83ED /* Sources */ = { 328 | isa = PBXSourcesBuildPhase; 329 | buildActionMask = 2147483647; 330 | files = ( 331 | 5F50E6EAC8DDE44079E03638 /* TZStackViewTests.swift in Sources */, 332 | DB5B70871B2B8816006043BD /* TZStackViewTestCase.swift in Sources */, 333 | DB5B70851B2A1963006043BD /* TestView.swift in Sources */, 334 | 5F50EAD959E8ACC5929DBD75 /* NSLayoutConstraintExtension.swift in Sources */, 335 | ); 336 | runOnlyForDeploymentPostprocessing = 0; 337 | }; 338 | 5F50EACD0CA47C3C2D5ECE2D /* Sources */ = { 339 | isa = PBXSourcesBuildPhase; 340 | buildActionMask = 2147483647; 341 | files = ( 342 | 5F50E81C644B9C88791038B0 /* AppDelegate.swift in Sources */, 343 | DB41AF6A1B294B8E003DB902 /* NSLayoutConstraintExtension.swift in Sources */, 344 | 5F50E83B164FAE28B52A5A91 /* ViewController.swift in Sources */, 345 | 5F50E9F2F7E5B2DA68C946E0 /* ExplicitIntrinsicContentSizeView.swift in Sources */, 346 | ); 347 | runOnlyForDeploymentPostprocessing = 0; 348 | }; 349 | A45441BA1B9B6D71002452BA /* Sources */ = { 350 | isa = PBXSourcesBuildPhase; 351 | buildActionMask = 2147483647; 352 | files = ( 353 | A45441D21B9B6D9C002452BA /* TZStackViewAlignment.swift in Sources */, 354 | A45441D01B9B6D9C002452BA /* TZSpacerView.swift in Sources */, 355 | A45441D11B9B6D9C002452BA /* TZStackView.swift in Sources */, 356 | A45441D31B9B6D9C002452BA /* TZStackViewDistribution.swift in Sources */, 357 | ); 358 | runOnlyForDeploymentPostprocessing = 0; 359 | }; 360 | /* End PBXSourcesBuildPhase section */ 361 | 362 | /* Begin PBXTargetDependency section */ 363 | 5F50E9A4787A8017DFBA09FB /* PBXTargetDependency */ = { 364 | isa = PBXTargetDependency; 365 | target = 5F50E36345E94CFC65ED7270 /* TZStackViewDemo */; 366 | targetProxy = 5F50E2D085856B752DDB3B93 /* PBXContainerItemProxy */; 367 | }; 368 | A45441C51B9B6D71002452BA /* PBXTargetDependency */ = { 369 | isa = PBXTargetDependency; 370 | target = A45441BE1B9B6D71002452BA /* TZStackView */; 371 | targetProxy = A45441C41B9B6D71002452BA /* PBXContainerItemProxy */; 372 | }; 373 | /* End PBXTargetDependency section */ 374 | 375 | /* Begin XCBuildConfiguration section */ 376 | 5F50E2A2200A464705C478D7 /* Debug */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 381 | CLANG_CXX_LIBRARY = "libc++"; 382 | CLANG_ENABLE_MODULES = YES; 383 | CLANG_ENABLE_OBJC_ARC = YES; 384 | CLANG_WARN_BOOL_CONVERSION = YES; 385 | CLANG_WARN_CONSTANT_CONVERSION = YES; 386 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 387 | CLANG_WARN_EMPTY_BODY = YES; 388 | CLANG_WARN_ENUM_CONVERSION = YES; 389 | CLANG_WARN_INFINITE_RECURSION = YES; 390 | CLANG_WARN_INT_CONVERSION = YES; 391 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 392 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 393 | CLANG_WARN_UNREACHABLE_CODE = YES; 394 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 395 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 396 | COPY_PHASE_STRIP = NO; 397 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 398 | ENABLE_STRICT_OBJC_MSGSEND = YES; 399 | ENABLE_TESTABILITY = YES; 400 | GCC_C_LANGUAGE_STANDARD = gnu99; 401 | GCC_DYNAMIC_NO_PIC = NO; 402 | GCC_NO_COMMON_BLOCKS = YES; 403 | GCC_OPTIMIZATION_LEVEL = 0; 404 | GCC_PREPROCESSOR_DEFINITIONS = ( 405 | "DEBUG=1", 406 | "$(inherited)", 407 | ); 408 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 409 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 410 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 411 | GCC_WARN_UNDECLARED_SELECTOR = YES; 412 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 413 | GCC_WARN_UNUSED_FUNCTION = YES; 414 | GCC_WARN_UNUSED_VARIABLE = YES; 415 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 416 | MTL_ENABLE_DEBUG_INFO = YES; 417 | ONLY_ACTIVE_ARCH = YES; 418 | SDKROOT = iphoneos; 419 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 420 | }; 421 | name = Debug; 422 | }; 423 | 5F50E2B435DD50B517F66006 /* Release */ = { 424 | isa = XCBuildConfiguration; 425 | buildSettings = { 426 | ALWAYS_SEARCH_USER_PATHS = NO; 427 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 428 | CLANG_CXX_LIBRARY = "libc++"; 429 | CLANG_ENABLE_MODULES = YES; 430 | CLANG_ENABLE_OBJC_ARC = YES; 431 | CLANG_WARN_BOOL_CONVERSION = YES; 432 | CLANG_WARN_CONSTANT_CONVERSION = YES; 433 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 434 | CLANG_WARN_EMPTY_BODY = YES; 435 | CLANG_WARN_ENUM_CONVERSION = YES; 436 | CLANG_WARN_INFINITE_RECURSION = YES; 437 | CLANG_WARN_INT_CONVERSION = YES; 438 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 439 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 440 | CLANG_WARN_UNREACHABLE_CODE = YES; 441 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 442 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 443 | COPY_PHASE_STRIP = NO; 444 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 445 | ENABLE_NS_ASSERTIONS = NO; 446 | ENABLE_STRICT_OBJC_MSGSEND = YES; 447 | GCC_C_LANGUAGE_STANDARD = gnu99; 448 | GCC_NO_COMMON_BLOCKS = YES; 449 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 450 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 451 | GCC_WARN_UNDECLARED_SELECTOR = YES; 452 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 453 | GCC_WARN_UNUSED_FUNCTION = YES; 454 | GCC_WARN_UNUSED_VARIABLE = YES; 455 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 456 | MTL_ENABLE_DEBUG_INFO = NO; 457 | SDKROOT = iphoneos; 458 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 459 | VALIDATE_PRODUCT = YES; 460 | }; 461 | name = Release; 462 | }; 463 | 5F50E2E44E5144E17840C724 /* Debug */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 467 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 468 | INFOPLIST_FILE = TZStackViewDemo/Info.plist; 469 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 470 | PRODUCT_BUNDLE_IDENTIFIER = "nl.tomvanzummeren.$(PRODUCT_NAME:rfc1034identifier)"; 471 | PRODUCT_NAME = "$(TARGET_NAME)"; 472 | SWIFT_VERSION = 3.0; 473 | }; 474 | name = Debug; 475 | }; 476 | 5F50E86DCFE460998B6EFE66 /* Release */ = { 477 | isa = XCBuildConfiguration; 478 | buildSettings = { 479 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/TZStackViewDemo.app/TZStackViewDemo"; 480 | FRAMEWORK_SEARCH_PATHS = ( 481 | "$(SDKROOT)/Developer/Library/Frameworks", 482 | "$(inherited)", 483 | ); 484 | INFOPLIST_FILE = TZStackViewTests/Info.plist; 485 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 486 | PRODUCT_BUNDLE_IDENTIFIER = "nl.tomvanzummeren.$(PRODUCT_NAME:rfc1034identifier)"; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | SWIFT_VERSION = 3.0; 489 | TEST_HOST = "$(BUNDLE_LOADER)"; 490 | }; 491 | name = Release; 492 | }; 493 | 5F50E984DD96284DF06BEF30 /* Debug */ = { 494 | isa = XCBuildConfiguration; 495 | buildSettings = { 496 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/TZStackViewDemo.app/TZStackViewDemo"; 497 | FRAMEWORK_SEARCH_PATHS = ( 498 | "$(SDKROOT)/Developer/Library/Frameworks", 499 | "$(inherited)", 500 | ); 501 | INFOPLIST_FILE = TZStackViewTests/Info.plist; 502 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 503 | PRODUCT_BUNDLE_IDENTIFIER = "nl.tomvanzummeren.$(PRODUCT_NAME:rfc1034identifier)"; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | SWIFT_VERSION = 3.0; 506 | TEST_HOST = "$(BUNDLE_LOADER)"; 507 | }; 508 | name = Debug; 509 | }; 510 | 5F50ECFE01A29CC56EA94F02 /* Release */ = { 511 | isa = XCBuildConfiguration; 512 | buildSettings = { 513 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 514 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 515 | INFOPLIST_FILE = TZStackViewDemo/Info.plist; 516 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 517 | PRODUCT_BUNDLE_IDENTIFIER = "nl.tomvanzummeren.$(PRODUCT_NAME:rfc1034identifier)"; 518 | PRODUCT_NAME = "$(TARGET_NAME)"; 519 | SWIFT_VERSION = 3.0; 520 | }; 521 | name = Release; 522 | }; 523 | A45441C91B9B6D71002452BA /* Debug */ = { 524 | isa = XCBuildConfiguration; 525 | buildSettings = { 526 | CLANG_ENABLE_MODULES = YES; 527 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 528 | CURRENT_PROJECT_VERSION = 1; 529 | DEBUG_INFORMATION_FORMAT = dwarf; 530 | DEFINES_MODULE = YES; 531 | DYLIB_COMPATIBILITY_VERSION = 1; 532 | DYLIB_CURRENT_VERSION = 1; 533 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 534 | INFOPLIST_FILE = TZStackView/Info.plist; 535 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 536 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 537 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 538 | PRODUCT_BUNDLE_IDENTIFIER = nl.tomvanzummeren.TZStackView; 539 | PRODUCT_NAME = "$(TARGET_NAME)"; 540 | SKIP_INSTALL = YES; 541 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 542 | SWIFT_VERSION = 3.0; 543 | TARGETED_DEVICE_FAMILY = "1,2"; 544 | VERSIONING_SYSTEM = "apple-generic"; 545 | VERSION_INFO_PREFIX = ""; 546 | }; 547 | name = Debug; 548 | }; 549 | A45441CA1B9B6D71002452BA /* Release */ = { 550 | isa = XCBuildConfiguration; 551 | buildSettings = { 552 | CLANG_ENABLE_MODULES = YES; 553 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 554 | CURRENT_PROJECT_VERSION = 1; 555 | DEFINES_MODULE = YES; 556 | DYLIB_COMPATIBILITY_VERSION = 1; 557 | DYLIB_CURRENT_VERSION = 1; 558 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 559 | INFOPLIST_FILE = TZStackView/Info.plist; 560 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 561 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 562 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 563 | PRODUCT_BUNDLE_IDENTIFIER = nl.tomvanzummeren.TZStackView; 564 | PRODUCT_NAME = "$(TARGET_NAME)"; 565 | SKIP_INSTALL = YES; 566 | SWIFT_VERSION = 3.0; 567 | TARGETED_DEVICE_FAMILY = "1,2"; 568 | VERSIONING_SYSTEM = "apple-generic"; 569 | VERSION_INFO_PREFIX = ""; 570 | }; 571 | name = Release; 572 | }; 573 | /* End XCBuildConfiguration section */ 574 | 575 | /* Begin XCConfigurationList section */ 576 | 5F50E4200B3FF23FD89C2CE5 /* Build configuration list for PBXNativeTarget "TZStackViewTests" */ = { 577 | isa = XCConfigurationList; 578 | buildConfigurations = ( 579 | 5F50E984DD96284DF06BEF30 /* Debug */, 580 | 5F50E86DCFE460998B6EFE66 /* Release */, 581 | ); 582 | defaultConfigurationIsVisible = 0; 583 | defaultConfigurationName = Release; 584 | }; 585 | 5F50EB67250401C8180503F9 /* Build configuration list for PBXNativeTarget "TZStackViewDemo" */ = { 586 | isa = XCConfigurationList; 587 | buildConfigurations = ( 588 | 5F50E2E44E5144E17840C724 /* Debug */, 589 | 5F50ECFE01A29CC56EA94F02 /* Release */, 590 | ); 591 | defaultConfigurationIsVisible = 0; 592 | defaultConfigurationName = Release; 593 | }; 594 | 5F50EBF385F683E8C65E5F50 /* Build configuration list for PBXProject "TZStackView" */ = { 595 | isa = XCConfigurationList; 596 | buildConfigurations = ( 597 | 5F50E2A2200A464705C478D7 /* Debug */, 598 | 5F50E2B435DD50B517F66006 /* Release */, 599 | ); 600 | defaultConfigurationIsVisible = 0; 601 | defaultConfigurationName = Release; 602 | }; 603 | A45441C81B9B6D71002452BA /* Build configuration list for PBXNativeTarget "TZStackView" */ = { 604 | isa = XCConfigurationList; 605 | buildConfigurations = ( 606 | A45441C91B9B6D71002452BA /* Debug */, 607 | A45441CA1B9B6D71002452BA /* Release */, 608 | ); 609 | defaultConfigurationIsVisible = 0; 610 | defaultConfigurationName = Release; 611 | }; 612 | /* End XCConfigurationList section */ 613 | }; 614 | rootObject = 5F50E8956739A06BEE120598 /* Project object */; 615 | } 616 | -------------------------------------------------------------------------------- /TZStackView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TZStackView.xcodeproj/xcshareddata/xcschemes/TZStackView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /TZStackView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /TZStackView/TZSpacerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TZSpacerView.swift 3 | // TZStackView 4 | // 5 | // Created by Tom van Zummeren on 15/06/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class TZSpacerView: UIView { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /TZStackView/TZStackView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TZStackView.h 3 | // TZStackView 4 | // 5 | // Created by Vincent Esche on 9/5/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TZStackView. 12 | FOUNDATION_EXPORT double TZStackViewVersionNumber; 13 | 14 | //! Project version string for TZStackView. 15 | FOUNDATION_EXPORT const unsigned char TZStackViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /TZStackView/TZStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TZStackView.swift 3 | // TZStackView 4 | // 5 | // Created by Tom van Zummeren on 10/06/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct TZAnimationDidStopQueueEntry: Equatable { 12 | let view: UIView 13 | let hidden: Bool 14 | } 15 | 16 | func ==(lhs: TZAnimationDidStopQueueEntry, rhs: TZAnimationDidStopQueueEntry) -> Bool { 17 | return lhs.view === rhs.view 18 | } 19 | 20 | public class TZStackView: UIView { 21 | 22 | public var distribution: TZStackViewDistribution = .fill { 23 | didSet { 24 | setNeedsUpdateConstraints() 25 | } 26 | } 27 | 28 | public var axis: UILayoutConstraintAxis = .horizontal { 29 | didSet { 30 | setNeedsUpdateConstraints() 31 | } 32 | } 33 | 34 | public var alignment: TZStackViewAlignment = .fill 35 | 36 | public var spacing: CGFloat = 0 37 | 38 | public var layoutMarginsRelativeArrangement = false 39 | 40 | public private(set) var arrangedSubviews: [UIView] = [] { 41 | didSet { 42 | setNeedsUpdateConstraints() 43 | registerHiddenListeners(oldValue) 44 | } 45 | } 46 | 47 | private var kvoContext = UInt8() 48 | 49 | private var stackViewConstraints = [NSLayoutConstraint]() 50 | private var subviewConstraints = [NSLayoutConstraint]() 51 | 52 | private var spacerViews = [UIView]() 53 | 54 | private var animationDidStopQueueEntries = [TZAnimationDidStopQueueEntry]() 55 | 56 | private var registeredKvoSubviews = [UIView]() 57 | 58 | private var animatingToHiddenViews = [UIView]() 59 | 60 | public init(arrangedSubviews: [UIView] = []) { 61 | super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) 62 | for arrangedSubview in arrangedSubviews { 63 | arrangedSubview.translatesAutoresizingMaskIntoConstraints = false 64 | addSubview(arrangedSubview) 65 | } 66 | 67 | // Closure to invoke didSet() 68 | { self.arrangedSubviews = arrangedSubviews }() 69 | } 70 | 71 | deinit { 72 | // This removes `hidden` value KVO observers using didSet() 73 | { self.arrangedSubviews = [] }() 74 | } 75 | 76 | private func registerHiddenListeners(_ previousArrangedSubviews: [UIView]) { 77 | for subview in previousArrangedSubviews { 78 | self.removeHiddenListener(subview) 79 | } 80 | 81 | for subview in arrangedSubviews { 82 | self.addHiddenListener(subview) 83 | } 84 | } 85 | 86 | private func addHiddenListener(_ view: UIView) { 87 | view.addObserver(self, forKeyPath: "hidden", options: [.old, .new], context: &kvoContext) 88 | registeredKvoSubviews.append(view) 89 | } 90 | 91 | private func removeHiddenListener(_ view: UIView) { 92 | if let index = registeredKvoSubviews.index(of: view) { 93 | view.removeObserver(self, forKeyPath: "hidden", context: &kvoContext) 94 | registeredKvoSubviews.remove(at: index) 95 | } 96 | } 97 | 98 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 99 | if let view = object as? UIView, let change = change, keyPath == "hidden" { 100 | let hidden = view.isHidden 101 | let previousValue = change[.oldKey] as! Bool 102 | if hidden == previousValue { 103 | return 104 | } 105 | if hidden { 106 | animatingToHiddenViews.append(view) 107 | } 108 | // Perform the animation 109 | setNeedsUpdateConstraints() 110 | setNeedsLayout() 111 | layoutIfNeeded() 112 | 113 | removeHiddenListener(view) 114 | view.isHidden = false 115 | 116 | if let _ = view.layer.animationKeys() { 117 | UIView.setAnimationDelegate(self) 118 | animationDidStopQueueEntries.insert(TZAnimationDidStopQueueEntry(view: view, hidden: hidden), at: 0) 119 | UIView.setAnimationDidStop(#selector(TZStackView.hiddenAnimationStopped)) 120 | } else { 121 | didFinishSettingHiddenValue(view, hidden: hidden) 122 | } 123 | } 124 | } 125 | 126 | private func didFinishSettingHiddenValue(_ arrangedSubview: UIView, hidden: Bool) { 127 | arrangedSubview.isHidden = hidden 128 | if let index = animatingToHiddenViews.index(of: arrangedSubview) { 129 | animatingToHiddenViews.remove(at: index) 130 | } 131 | addHiddenListener(arrangedSubview) 132 | } 133 | 134 | func hiddenAnimationStopped() { 135 | var queueEntriesToRemove = [TZAnimationDidStopQueueEntry]() 136 | for entry in animationDidStopQueueEntries { 137 | let view = entry.view 138 | if view.layer.animationKeys() == nil { 139 | didFinishSettingHiddenValue(view, hidden: entry.hidden) 140 | queueEntriesToRemove.append(entry) 141 | } 142 | } 143 | for entry in queueEntriesToRemove { 144 | if let index = animationDidStopQueueEntries.index(of: entry) { 145 | animationDidStopQueueEntries.remove(at: index) 146 | } 147 | } 148 | } 149 | 150 | public func addArrangedSubview(_ view: UIView) { 151 | view.translatesAutoresizingMaskIntoConstraints = false 152 | addSubview(view) 153 | arrangedSubviews.append(view) 154 | } 155 | 156 | public func removeArrangedSubview(_ view: UIView) { 157 | if let index = arrangedSubviews.index(of: view) { 158 | arrangedSubviews.remove(at: index) 159 | } 160 | } 161 | 162 | public func insertArrangedSubview(_ view: UIView, atIndex stackIndex: Int) { 163 | view.translatesAutoresizingMaskIntoConstraints = false 164 | addSubview(view) 165 | arrangedSubviews.insert(view, at: stackIndex) 166 | } 167 | 168 | override public func willRemoveSubview(_ subview: UIView) { 169 | removeArrangedSubview(subview) 170 | } 171 | 172 | override public func updateConstraints() { 173 | removeConstraints(stackViewConstraints) 174 | stackViewConstraints.removeAll() 175 | 176 | for arrangedSubview in arrangedSubviews { 177 | arrangedSubview.removeConstraints(subviewConstraints) 178 | } 179 | subviewConstraints.removeAll() 180 | for arrangedSubview in arrangedSubviews { 181 | 182 | if alignment != .fill { 183 | let guideConstraint: NSLayoutConstraint 184 | switch axis { 185 | case .horizontal: 186 | guideConstraint = constraint(item: arrangedSubview, attribute: .height, toItem: nil, attribute: .notAnAttribute, constant: 0, priority: 25) 187 | case .vertical: 188 | guideConstraint = constraint(item: arrangedSubview, attribute: .width, toItem: nil, attribute: .notAnAttribute, constant: 0, priority: 25) 189 | } 190 | subviewConstraints.append(guideConstraint) 191 | arrangedSubview.addConstraint(guideConstraint) 192 | } 193 | 194 | if isHidden(arrangedSubview) { 195 | let hiddenConstraint: NSLayoutConstraint 196 | switch axis { 197 | case .horizontal: 198 | hiddenConstraint = constraint(item: arrangedSubview, attribute: .width, toItem: nil, attribute: .notAnAttribute, constant: 0) 199 | case .vertical: 200 | hiddenConstraint = constraint(item: arrangedSubview, attribute: .height, toItem: nil, attribute: .notAnAttribute, constant: 0) 201 | } 202 | subviewConstraints.append(hiddenConstraint) 203 | arrangedSubview.addConstraint(hiddenConstraint) 204 | } 205 | } 206 | 207 | for spacerView in spacerViews { 208 | spacerView.removeFromSuperview() 209 | } 210 | spacerViews.removeAll() 211 | 212 | if arrangedSubviews.count > 0 { 213 | 214 | let visibleArrangedSubviews = arrangedSubviews.filter({!self.isHidden($0)}) 215 | 216 | switch distribution { 217 | case .fillEqually, .fill, .fillProportionally: 218 | if alignment != .fill || layoutMarginsRelativeArrangement { 219 | _ = addSpacerView() 220 | } 221 | 222 | stackViewConstraints += createMatchEdgesContraints(arrangedSubviews) 223 | stackViewConstraints += createFirstAndLastViewMatchEdgesContraints() 224 | 225 | if alignment == .firstBaseline && axis == .horizontal { 226 | stackViewConstraints.append(constraint(item: self, attribute: .height, toItem: nil, attribute: .notAnAttribute, priority: 49)) 227 | } 228 | 229 | if distribution == .fillEqually { 230 | stackViewConstraints += createFillEquallyConstraints(arrangedSubviews) 231 | } 232 | if distribution == .fillProportionally { 233 | stackViewConstraints += createFillProportionallyConstraints(arrangedSubviews) 234 | } 235 | 236 | stackViewConstraints += createFillConstraints(arrangedSubviews, constant: spacing) 237 | case .equalSpacing: 238 | var views = [UIView]() 239 | var index = 0 240 | for arrangedSubview in arrangedSubviews { 241 | if isHidden(arrangedSubview) { 242 | continue 243 | } 244 | if index > 0 { 245 | views.append(addSpacerView()) 246 | } 247 | views.append(arrangedSubview) 248 | index += 1 249 | } 250 | if spacerViews.count == 0 { 251 | _ = addSpacerView() 252 | } 253 | 254 | stackViewConstraints += createMatchEdgesContraints(arrangedSubviews) 255 | stackViewConstraints += createFirstAndLastViewMatchEdgesContraints() 256 | 257 | switch axis { 258 | case .horizontal: 259 | stackViewConstraints.append(constraint(item: self, attribute: .width, toItem: nil, attribute: .notAnAttribute, priority: 49)) 260 | if alignment == .firstBaseline { 261 | stackViewConstraints.append(constraint(item: self, attribute: .height, toItem: nil, attribute: .notAnAttribute, priority: 49)) 262 | } 263 | case .vertical: 264 | stackViewConstraints.append(constraint(item: self, attribute: .height, toItem: nil, attribute: .notAnAttribute, priority: 49)) 265 | } 266 | 267 | stackViewConstraints += createFillConstraints(views, constant: 0) 268 | stackViewConstraints += createFillEquallyConstraints(spacerViews) 269 | stackViewConstraints += createFillConstraints(arrangedSubviews, relatedBy: .greaterThanOrEqual, constant: spacing) 270 | case .equalCentering: 271 | for (index, _) in visibleArrangedSubviews.enumerated() { 272 | if index > 0 { 273 | _ = addSpacerView() 274 | } 275 | } 276 | if spacerViews.count == 0 { 277 | _ = addSpacerView() 278 | } 279 | 280 | stackViewConstraints += createMatchEdgesContraints(arrangedSubviews) 281 | stackViewConstraints += createFirstAndLastViewMatchEdgesContraints() 282 | 283 | switch axis { 284 | case .horizontal: 285 | stackViewConstraints.append(constraint(item: self, attribute: .width, toItem: nil, attribute: .notAnAttribute, priority: 49)) 286 | if alignment == .firstBaseline { 287 | stackViewConstraints.append(constraint(item: self, attribute: .height, toItem: nil, attribute: .notAnAttribute, priority: 49)) 288 | } 289 | case .vertical: 290 | stackViewConstraints.append(constraint(item: self, attribute: .height, toItem: nil, attribute: .notAnAttribute, priority: 49)) 291 | } 292 | 293 | var previousArrangedSubview: UIView? 294 | for (index, arrangedSubview) in visibleArrangedSubviews.enumerated() { 295 | if let previousArrangedSubview = previousArrangedSubview { 296 | let spacerView = spacerViews[index - 1] 297 | 298 | switch axis { 299 | case .horizontal: 300 | stackViewConstraints.append(constraint(item: previousArrangedSubview, attribute: .centerX, toItem: spacerView, attribute: .leading)) 301 | stackViewConstraints.append(constraint(item: arrangedSubview, attribute: .centerX, toItem: spacerView, attribute: .trailing)) 302 | case .vertical: 303 | stackViewConstraints.append(constraint(item: previousArrangedSubview, attribute: .centerY, toItem: spacerView, attribute: .top)) 304 | stackViewConstraints.append(constraint(item: arrangedSubview, attribute: .centerY, toItem: spacerView, attribute: .bottom)) 305 | } 306 | } 307 | previousArrangedSubview = arrangedSubview 308 | } 309 | 310 | stackViewConstraints += createFillEquallyConstraints(spacerViews, priority: 150) 311 | stackViewConstraints += createFillConstraints(arrangedSubviews, relatedBy: .greaterThanOrEqual, constant: spacing) 312 | } 313 | 314 | if spacerViews.count > 0 { 315 | stackViewConstraints += createSurroundingSpacerViewConstraints(spacerViews[0], views: visibleArrangedSubviews) 316 | } 317 | 318 | if layoutMarginsRelativeArrangement { 319 | if spacerViews.count > 0 { 320 | stackViewConstraints.append(constraint(item: self, attribute: .bottomMargin, toItem: spacerViews[0], attribute: .bottom)) 321 | stackViewConstraints.append(constraint(item: self, attribute: .leftMargin, toItem: spacerViews[0], attribute: .left)) 322 | stackViewConstraints.append(constraint(item: self, attribute: .rightMargin, toItem: spacerViews[0], attribute: .right)) 323 | stackViewConstraints.append(constraint(item: self, attribute: .topMargin, toItem: spacerViews[0], attribute: .top)) 324 | } 325 | } 326 | addConstraints(stackViewConstraints) 327 | } 328 | 329 | super.updateConstraints() 330 | } 331 | 332 | required public init(coder aDecoder: NSCoder) { 333 | super.init(coder: aDecoder)! 334 | } 335 | 336 | private func addSpacerView() -> TZSpacerView { 337 | let spacerView = TZSpacerView() 338 | spacerView.translatesAutoresizingMaskIntoConstraints = false 339 | 340 | spacerViews.append(spacerView) 341 | insertSubview(spacerView, at: 0) 342 | return spacerView 343 | } 344 | 345 | private func createSurroundingSpacerViewConstraints(_ spacerView: UIView, views: [UIView]) -> [NSLayoutConstraint] { 346 | if alignment == .fill { 347 | return [] 348 | } 349 | 350 | var topPriority: Float = 1000 351 | var topRelation: NSLayoutRelation = .lessThanOrEqual 352 | 353 | var bottomPriority: Float = 1000 354 | var bottomRelation: NSLayoutRelation = .greaterThanOrEqual 355 | 356 | if alignment == .top || alignment == .leading { 357 | topPriority = 999.5 358 | topRelation = .equal 359 | } 360 | 361 | if alignment == .bottom || alignment == .trailing { 362 | bottomPriority = 999.5 363 | bottomRelation = .equal 364 | } 365 | 366 | var constraints = [NSLayoutConstraint]() 367 | for view in views { 368 | switch axis { 369 | case .horizontal: 370 | constraints.append(constraint(item: spacerView, attribute: .top, relatedBy: topRelation, toItem: view, priority: topPriority)) 371 | constraints.append(constraint(item: spacerView, attribute: .bottom, relatedBy: bottomRelation, toItem: view, priority: bottomPriority)) 372 | case .vertical: 373 | constraints.append(constraint(item: spacerView, attribute: .leading, relatedBy: topRelation, toItem: view, priority: topPriority)) 374 | constraints.append(constraint(item: spacerView, attribute: .trailing, relatedBy: bottomRelation, toItem: view, priority: bottomPriority)) 375 | } 376 | } 377 | switch axis { 378 | case .horizontal: 379 | constraints.append(constraint(item: spacerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, constant: 0, priority: 51)) 380 | case .vertical: 381 | constraints.append(constraint(item: spacerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, constant: 0, priority: 51)) 382 | } 383 | return constraints 384 | } 385 | 386 | private func createFillProportionallyConstraints(_ views: [UIView]) -> [NSLayoutConstraint] { 387 | var constraints = [NSLayoutConstraint]() 388 | 389 | var totalSize: CGFloat = 0 390 | var totalCount = 0 391 | for arrangedSubview in views { 392 | if isHidden(arrangedSubview) { 393 | continue 394 | } 395 | switch axis { 396 | case .horizontal: 397 | totalSize += arrangedSubview.intrinsicContentSize.width 398 | case .vertical: 399 | totalSize += arrangedSubview.intrinsicContentSize.height 400 | } 401 | totalCount += 1 402 | } 403 | totalSize += (CGFloat(totalCount - 1) * spacing) 404 | 405 | var priority: Float = 1000 406 | let countDownPriority = (views.filter({!self.isHidden($0)}).count > 1) 407 | for arrangedSubview in views { 408 | if countDownPriority { 409 | priority -= 1 410 | } 411 | 412 | if isHidden(arrangedSubview) { 413 | continue 414 | } 415 | switch axis { 416 | case .horizontal: 417 | let multiplier = arrangedSubview.intrinsicContentSize.width / totalSize 418 | constraints.append(constraint(item: arrangedSubview, attribute: .width, toItem: self, multiplier: multiplier, priority: priority)) 419 | case .vertical: 420 | let multiplier = arrangedSubview.intrinsicContentSize.height / totalSize 421 | constraints.append(constraint(item: arrangedSubview, attribute: .height, toItem: self, multiplier: multiplier, priority: priority)) 422 | } 423 | } 424 | 425 | return constraints 426 | } 427 | 428 | // Matchs all Width or Height attributes of all given views 429 | private func createFillEquallyConstraints(_ views: [UIView], priority: Float = 1000) -> [NSLayoutConstraint] { 430 | switch axis { 431 | case .horizontal: 432 | return equalAttributes(views: views.filter({ !self.isHidden($0) }), attribute: .width, priority: priority) 433 | 434 | case .vertical: 435 | return equalAttributes(views: views.filter({ !self.isHidden($0) }), attribute: .height, priority: priority) 436 | } 437 | } 438 | 439 | // Chains together the given views using Leading/Trailing or Top/Bottom 440 | private func createFillConstraints(_ views: [UIView], priority: Float = 1000, relatedBy relation: NSLayoutRelation = .equal, constant: CGFloat) -> [NSLayoutConstraint] { 441 | var constraints = [NSLayoutConstraint]() 442 | 443 | var previousView: UIView? 444 | for view in views { 445 | if let previousView = previousView { 446 | var c: CGFloat = 0 447 | if !isHidden(previousView) && !isHidden(view) { 448 | c = constant 449 | } else if isHidden(previousView) && !isHidden(view) && views.first != previousView { 450 | c = (constant / 2) 451 | } else if isHidden(view) && !isHidden(previousView) && views.last != view { 452 | c = (constant / 2) 453 | } 454 | switch axis { 455 | case .horizontal: 456 | constraints.append(constraint(item: view, attribute: .leading, relatedBy: relation, toItem: previousView, attribute: .trailing, constant: c, priority: priority)) 457 | 458 | case .vertical: 459 | constraints.append(constraint(item: view, attribute: .top, relatedBy: relation, toItem: previousView, attribute: .bottom, constant: c, priority: priority)) 460 | } 461 | } 462 | previousView = view 463 | } 464 | return constraints 465 | } 466 | 467 | // Matches all Bottom/Top or Leading Trailing constraints of te given views and matches those attributes of the first/last view to the container 468 | private func createMatchEdgesContraints(_ views: [UIView]) -> [NSLayoutConstraint] { 469 | var constraints = [NSLayoutConstraint]() 470 | 471 | switch axis { 472 | case .horizontal: 473 | switch alignment { 474 | case .fill: 475 | constraints += equalAttributes(views: views, attribute: .bottom) 476 | constraints += equalAttributes(views: views, attribute: .top) 477 | case .center: 478 | constraints += equalAttributes(views: views, attribute: .centerY) 479 | case .leading, .top: 480 | constraints += equalAttributes(views: views, attribute: .top) 481 | case .trailing, .bottom: 482 | constraints += equalAttributes(views: views, attribute: .bottom) 483 | case .firstBaseline: 484 | constraints += equalAttributes(views: views, attribute: .firstBaseline) 485 | } 486 | 487 | case .vertical: 488 | switch alignment { 489 | case .fill: 490 | constraints += equalAttributes(views: views, attribute: .leading) 491 | constraints += equalAttributes(views: views, attribute: .trailing) 492 | case .center: 493 | constraints += equalAttributes(views: views, attribute: .centerX) 494 | case .leading, .top: 495 | constraints += equalAttributes(views: views, attribute: .leading) 496 | case .trailing, .bottom: 497 | constraints += equalAttributes(views: views, attribute: .trailing) 498 | case .firstBaseline: 499 | constraints += [] 500 | } 501 | } 502 | return constraints 503 | } 504 | 505 | private func createFirstAndLastViewMatchEdgesContraints() -> [NSLayoutConstraint] { 506 | 507 | var constraints = [NSLayoutConstraint]() 508 | 509 | let visibleViews = arrangedSubviews.filter({!self.isHidden($0)}) 510 | let firstView = visibleViews.first 511 | let lastView = visibleViews.last 512 | 513 | var topView = arrangedSubviews.first! 514 | var bottomView = arrangedSubviews.first! 515 | if spacerViews.count > 0 { 516 | if alignment == .center { 517 | topView = spacerViews[0] 518 | bottomView = spacerViews[0] 519 | } else if alignment == .top || alignment == .leading { 520 | bottomView = spacerViews[0] 521 | } else if alignment == .bottom || alignment == .trailing { 522 | topView = spacerViews[0] 523 | } else if alignment == .firstBaseline { 524 | switch axis { 525 | case .horizontal: 526 | bottomView = spacerViews[0] 527 | case .vertical: 528 | topView = spacerViews[0] 529 | bottomView = spacerViews[0] 530 | } 531 | } 532 | } 533 | 534 | let firstItem = layoutMarginsRelativeArrangement ? spacerViews[0] : self 535 | 536 | switch axis { 537 | case .horizontal: 538 | if let firstView = firstView { 539 | constraints.append(constraint(item: firstItem, attribute: .leading, toItem: firstView)) 540 | } 541 | if let lastView = lastView { 542 | constraints.append(constraint(item: firstItem, attribute: .trailing, toItem: lastView)) 543 | } 544 | 545 | constraints.append(constraint(item: firstItem, attribute: .top, toItem: topView)) 546 | constraints.append(constraint(item: firstItem, attribute: .bottom, toItem: bottomView)) 547 | 548 | if alignment == .center { 549 | constraints.append(constraint(item: firstItem, attribute: .centerY, toItem: arrangedSubviews.first!)) 550 | } 551 | case .vertical: 552 | if let firstView = firstView { 553 | constraints.append(constraint(item: firstItem, attribute: .top, toItem: firstView)) 554 | } 555 | if let lastView = lastView { 556 | constraints.append(constraint(item: firstItem, attribute: .bottom, toItem: lastView)) 557 | } 558 | 559 | constraints.append(constraint(item: firstItem, attribute: .leading, toItem: topView)) 560 | constraints.append(constraint(item: firstItem, attribute: .trailing, toItem: bottomView)) 561 | 562 | if alignment == .center { 563 | constraints.append(constraint(item: firstItem, attribute: .centerX, toItem: arrangedSubviews.first!)) 564 | } 565 | } 566 | 567 | return constraints 568 | } 569 | 570 | private func equalAttributes(views: [UIView], attribute: NSLayoutAttribute, priority: Float = 1000) -> [NSLayoutConstraint] { 571 | var currentPriority = priority 572 | var constraints = [NSLayoutConstraint]() 573 | if views.count > 0 { 574 | 575 | var firstView: UIView? 576 | 577 | let countDownPriority = (currentPriority < 1000) 578 | for view in views { 579 | if let firstView = firstView { 580 | constraints.append(constraint(item: firstView, attribute: attribute, toItem: view, priority: currentPriority)) 581 | } else { 582 | firstView = view 583 | } 584 | if countDownPriority { 585 | currentPriority -= 1 586 | } 587 | } 588 | } 589 | return constraints 590 | } 591 | 592 | // Convenience method to help make NSLayoutConstraint in a less verbose way 593 | private func constraint(item view1: AnyObject, attribute attr1: NSLayoutAttribute, relatedBy relation: NSLayoutRelation = .equal, toItem view2: AnyObject?, attribute attr2: NSLayoutAttribute? = nil, multiplier: CGFloat = 1, constant c: CGFloat = 0, priority: Float = 1000) -> NSLayoutConstraint { 594 | 595 | let attribute2 = attr2 != nil ? attr2! : attr1 596 | 597 | let constraint = NSLayoutConstraint(item: view1, attribute: attr1, relatedBy: relation, toItem: view2, attribute: attribute2, multiplier: multiplier, constant: c) 598 | constraint.priority = priority 599 | return constraint 600 | } 601 | 602 | private func isHidden(_ view: UIView) -> Bool { 603 | if view.isHidden { 604 | return true 605 | } 606 | return animatingToHiddenViews.index(of: view) != nil 607 | } 608 | } 609 | -------------------------------------------------------------------------------- /TZStackView/TZStackViewAlignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TZStackViewAlignment.swift 3 | // TZStackView 4 | // 5 | // Created by Tom van Zummeren on 15/06/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc public enum TZStackViewAlignment: Int { 12 | case fill 13 | case center 14 | case leading 15 | case top 16 | case trailing 17 | case bottom 18 | case firstBaseline 19 | } 20 | -------------------------------------------------------------------------------- /TZStackView/TZStackViewDistribution.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TZStackViewDistribution.swift 3 | // TZStackView 4 | // 5 | // Created by Tom van Zummeren on 10/06/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | @objc public enum TZStackViewDistribution: Int { 11 | case fill 12 | case fillEqually 13 | case fillProportionally 14 | case equalSpacing 15 | case equalCentering 16 | } 17 | -------------------------------------------------------------------------------- /TZStackViewDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // AppDelegate.swift 4 | // TZStackView-Example 5 | // 6 | // Created by Tom van Zummeren on 20/06/15. 7 | // Copyright (c) 2015 Tom van Zummeren. All rights reserved. 8 | // 9 | 10 | import UIKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | //Appearance proxy for the UISegmentedControl font 19 | UISegmentedControl.appearance().setTitleTextAttributes( 20 | [NSFontAttributeName:UIFont(name: "HelveticaNeue-Light", size:10.0)!], 21 | for: UIControlState()) 22 | 23 | //Launch the application 24 | window = UIWindow(frame: UIScreen.main.bounds) 25 | window?.rootViewController = ViewController() 26 | window?.makeKeyAndVisible() 27 | return true 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /TZStackViewDemo/ExplicitIntrinsicContentSizeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tom van Zummeren on 10/06/15. 3 | // Copyright (c) 2015 Tom van Zummeren. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | 8 | class ExplicitIntrinsicContentSizeView: UIView { 9 | 10 | let name: String 11 | let contentSize: CGSize 12 | 13 | init(intrinsicContentSize: CGSize, name: String) { 14 | self.name = name 15 | self.contentSize = intrinsicContentSize 16 | super.init(frame: CGRect.zero) 17 | 18 | let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ExplicitIntrinsicContentSizeView.tap)) 19 | addGestureRecognizer(gestureRecognizer) 20 | isUserInteractionEnabled = true 21 | } 22 | 23 | func tap() { 24 | UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: .allowUserInteraction, animations: { 25 | self.isHidden = true 26 | }, completion: nil) 27 | } 28 | 29 | required init(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | override var intrinsicContentSize : CGSize { 34 | return contentSize 35 | } 36 | 37 | override var description: String { 38 | return name 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /TZStackViewDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /TZStackViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarStyle 32 | UIStatusBarStyleLightContent 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /TZStackViewDemo/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /TZStackViewDemo/NSLayoutConstraintExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraintExtension.swift 3 | // TZStackView 4 | // 5 | // Created by Tom van Zummeren on 10/06/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSLayoutConstraint { 12 | 13 | func readableString() -> String { 14 | return "\(type(of: self))(\n item1: \(unwrap(firstItem)), firstAttribute: \(toString(firstAttribute))\n relation: \(toString(relation))\n secondItem: \(unwrap(secondItem)), secondAttribute: \(toString(secondAttribute))\n constant: \(constant)\n multiplier: \(multiplier)\n priority: \(priority)\n)" 15 | } 16 | 17 | fileprivate func unwrap(_ object: AnyObject?) -> String { 18 | if let object = object { 19 | return "\(object)" 20 | } 21 | return "nil" 22 | } 23 | 24 | fileprivate func toString(_ relation: NSLayoutRelation) -> String { 25 | switch relation { 26 | case .lessThanOrEqual: return "LessThanOrEqual" 27 | case .equal: return "Equal" 28 | case .greaterThanOrEqual: return "GreaterThanOrEqual" 29 | } 30 | } 31 | 32 | fileprivate func toString(_ attribute: NSLayoutAttribute) -> String { 33 | switch attribute { 34 | case .left: return "Left" 35 | case .right: return "Right" 36 | case .top: return "Top" 37 | case .bottom: return "Bottom" 38 | case .leading: return "Leading" 39 | case .trailing: return "Trailing" 40 | case .width: return "Width" 41 | case .height: return "Height" 42 | case .centerX: return "CenterX" 43 | case .centerY: return "CenterY" 44 | case .lastBaseline: return "Baseline" 45 | case .firstBaseline: return "FirstBaseline" 46 | case .notAnAttribute: return "NotAnAttribute" 47 | case .leftMargin: return "LeftMargin" 48 | case .rightMargin: return "RightMargin" 49 | case .topMargin: return "TopMargin" 50 | case .bottomMargin: return "BottomMargin" 51 | case .leadingMargin: return "LeadingMargin" 52 | case .trailingMargin: return "TrailingMargin" 53 | case .centerXWithinMargins: return "CenterXWithinMargins" 54 | case .centerYWithinMargins: return "CenterYWithinMargins" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /TZStackViewDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TZStackView-Example 4 | // 5 | // Created by Tom van Zummeren on 20/06/15. 6 | // Copyright (c) 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import TZStackView 12 | 13 | class ViewController: UIViewController { 14 | //MARK: - Properties 15 | //-------------------------------------------------------------------------- 16 | var tzStackView: TZStackView! 17 | 18 | let resetButton = UIButton(type: .system) 19 | let instructionLabel = UILabel() 20 | 21 | var axisSegmentedControl: UISegmentedControl! 22 | var alignmentSegmentedControl: UISegmentedControl! 23 | var distributionSegmentedControl: UISegmentedControl! 24 | 25 | //MARK: - Lifecyle 26 | //-------------------------------------------------------------------------- 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | edgesForExtendedLayout = UIRectEdge(); 30 | 31 | view.backgroundColor = UIColor.black 32 | title = "TZStackView" 33 | 34 | tzStackView = TZStackView(arrangedSubviews: createViews()) 35 | tzStackView.translatesAutoresizingMaskIntoConstraints = false 36 | tzStackView.axis = .vertical 37 | tzStackView.distribution = .fill 38 | tzStackView.alignment = .fill 39 | tzStackView.spacing = 15 40 | view.addSubview(tzStackView) 41 | 42 | instructionLabel.translatesAutoresizingMaskIntoConstraints = false 43 | instructionLabel.font = UIFont.systemFont(ofSize: 15) 44 | instructionLabel.text = "Tap any of the boxes to set hidden=true" 45 | instructionLabel.textColor = UIColor.white 46 | instructionLabel.numberOfLines = 0 47 | instructionLabel.setContentCompressionResistancePriority(900, for: .horizontal) 48 | instructionLabel.setContentHuggingPriority(1000, for: .vertical) 49 | view.addSubview(instructionLabel) 50 | 51 | resetButton.translatesAutoresizingMaskIntoConstraints = false 52 | resetButton.setTitle("Reset", for: UIControlState()) 53 | resetButton.addTarget(self, action: #selector(ViewController.reset), for: .touchUpInside) 54 | resetButton.setContentCompressionResistancePriority(1000, for: .horizontal) 55 | resetButton.setContentHuggingPriority(1000, for: .horizontal) 56 | resetButton.setContentHuggingPriority(1000, for: .vertical) 57 | view.addSubview(resetButton) 58 | 59 | axisSegmentedControl = UISegmentedControl(items: ["Vertical", "Horizontal"]) 60 | axisSegmentedControl.selectedSegmentIndex = 0 61 | axisSegmentedControl.addTarget(self, action: #selector(ViewController.axisChanged(_:)), for: .valueChanged) 62 | axisSegmentedControl.setContentCompressionResistancePriority(900, for: .horizontal) 63 | axisSegmentedControl.tintColor = UIColor.lightGray 64 | 65 | alignmentSegmentedControl = UISegmentedControl(items: ["Fill", "Center", "Leading", "Top", "Trailing", "Bottom", "FirstBaseline"]) 66 | alignmentSegmentedControl.selectedSegmentIndex = 0 67 | alignmentSegmentedControl.addTarget(self, action: #selector(ViewController.alignmentChanged(_:)), for: .valueChanged) 68 | alignmentSegmentedControl.setContentCompressionResistancePriority(1000, for: .horizontal) 69 | alignmentSegmentedControl.tintColor = UIColor.lightGray 70 | 71 | distributionSegmentedControl = UISegmentedControl(items: ["Fill", "FillEqually", "FillProportionally", "EqualSpacing", "EqualCentering"]) 72 | distributionSegmentedControl.selectedSegmentIndex = 0 73 | distributionSegmentedControl.addTarget(self, action: #selector(ViewController.distributionChanged(_:)), for: .valueChanged) 74 | distributionSegmentedControl.tintColor = UIColor.lightGray 75 | 76 | let controlsLayoutContainer = TZStackView(arrangedSubviews: [axisSegmentedControl, alignmentSegmentedControl, distributionSegmentedControl]) 77 | controlsLayoutContainer.axis = .vertical 78 | controlsLayoutContainer.translatesAutoresizingMaskIntoConstraints = false 79 | controlsLayoutContainer.spacing = 5 80 | controlsLayoutContainer.setContentHuggingPriority(1000, for: .vertical) 81 | view.addSubview(controlsLayoutContainer) 82 | 83 | let views: [String:AnyObject] = [ 84 | "instructionLabel": instructionLabel, 85 | "resetButton": resetButton, 86 | "tzStackView": tzStackView, 87 | "controlsLayoutContainer": controlsLayoutContainer 88 | ] 89 | 90 | let metrics: [String:AnyObject] = [ 91 | "gap": 10 as AnyObject, 92 | "topspacing": 25 as AnyObject 93 | ] 94 | 95 | view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-gap-[instructionLabel]-[resetButton]-gap-|", 96 | options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) 97 | view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[tzStackView]|", 98 | options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) 99 | view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[controlsLayoutContainer]|", 100 | options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) 101 | 102 | view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-topspacing-[instructionLabel]-gap-[controlsLayoutContainer]-gap-[tzStackView]|", 103 | options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) 104 | view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-topspacing-[resetButton]-gap-[controlsLayoutContainer]", 105 | options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views)) 106 | } 107 | 108 | fileprivate func createViews() -> [UIView] { 109 | let redView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 100, height: 100), name: "Red") 110 | let greenView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 80, height: 80), name: "Green") 111 | let blueView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 60, height: 60), name: "Blue") 112 | let purpleView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 80, height: 80), name: "Purple") 113 | let yellowView = ExplicitIntrinsicContentSizeView(intrinsicContentSize: CGSize(width: 100, height: 100), name: "Yellow") 114 | 115 | redView.backgroundColor = UIColor.red.withAlphaComponent(0.75) 116 | greenView.backgroundColor = UIColor.green.withAlphaComponent(0.75) 117 | blueView.backgroundColor = UIColor.blue.withAlphaComponent(0.75) 118 | purpleView.backgroundColor = UIColor.purple.withAlphaComponent(0.75) 119 | yellowView.backgroundColor = UIColor.yellow.withAlphaComponent(0.75) 120 | return [redView, greenView, blueView, purpleView, yellowView] 121 | } 122 | 123 | //MARK: - Button Actions 124 | //-------------------------------------------------------------------------- 125 | func reset() { 126 | UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: .allowUserInteraction, animations: { 127 | for view in self.tzStackView.arrangedSubviews { 128 | view.isHidden = false 129 | } 130 | }, completion: nil) 131 | 132 | } 133 | 134 | //MARK: - Segmented Control Actions 135 | //-------------------------------------------------------------------------- 136 | func axisChanged(_ sender: UISegmentedControl) { 137 | switch sender.selectedSegmentIndex { 138 | case 0: 139 | tzStackView.axis = .vertical 140 | default: 141 | tzStackView.axis = .horizontal 142 | } 143 | } 144 | 145 | func alignmentChanged(_ sender: UISegmentedControl) { 146 | switch sender.selectedSegmentIndex { 147 | case 0: 148 | tzStackView.alignment = .fill 149 | case 1: 150 | tzStackView.alignment = .center 151 | case 2: 152 | tzStackView.alignment = .leading 153 | case 3: 154 | tzStackView.alignment = .top 155 | case 4: 156 | tzStackView.alignment = .trailing 157 | case 5: 158 | tzStackView.alignment = .bottom 159 | default: 160 | tzStackView.alignment = .firstBaseline 161 | } 162 | tzStackView.setNeedsUpdateConstraints() 163 | } 164 | 165 | func distributionChanged(_ sender: UISegmentedControl) { 166 | switch sender.selectedSegmentIndex { 167 | case 0: 168 | tzStackView.distribution = .fill 169 | case 1: 170 | tzStackView.distribution = .fillEqually 171 | case 2: 172 | tzStackView.distribution = .fillProportionally 173 | case 3: 174 | tzStackView.distribution = .equalSpacing 175 | default: 176 | tzStackView.distribution = .equalCentering 177 | } 178 | } 179 | 180 | override var preferredStatusBarStyle : UIStatusBarStyle { 181 | return .lightContent 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /TZStackViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /TZStackViewTests/TZStackViewTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackViewTest.swift 3 | // TZStackView 4 | // 5 | // Created by Tom van Zummeren on 12/06/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | import TZStackView 13 | 14 | func delay(_ delay:Double, closure:@escaping ()->()) { 15 | DispatchQueue.main.asyncAfter( 16 | deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) 17 | } 18 | 19 | class TZStackViewTestCase: XCTestCase { 20 | 21 | var uiStackView: UIStackView! 22 | var tzStackView: TZStackView! 23 | 24 | override func setUp() { 25 | super.setUp() 26 | // Create stack views with same views 27 | uiStackView = UIStackView(arrangedSubviews: createTestViews()) 28 | uiStackView.translatesAutoresizingMaskIntoConstraints = false 29 | tzStackView = TZStackView(arrangedSubviews: createTestViews()) 30 | tzStackView.translatesAutoresizingMaskIntoConstraints = false 31 | 32 | let window = UIApplication.shared.windows[0] 33 | window.addSubview(uiStackView) 34 | window.addSubview(tzStackView) 35 | 36 | configureStackViews(uiStackView, tzStackView) 37 | } 38 | 39 | override func tearDown() { 40 | super.tearDown() 41 | } 42 | 43 | func logAllConstraints() { 44 | print("================= UISTACKVIEW (\(uiStackView.constraints.count)) =================") 45 | print("subviews: \(uiStackView.subviews)") 46 | printConstraints(uiStackView.constraints) 47 | print("") 48 | for subview in uiStackView.arrangedSubviews { 49 | print("\(subview):") 50 | printConstraints(nonContentSizeLayoutConstraints(subview)) 51 | } 52 | 53 | print("================= TZSTACKVIEW (\(tzStackView.constraints.count)) =================") 54 | print("subviews: \(tzStackView.subviews)") 55 | printConstraints(tzStackView.constraints) 56 | print("") 57 | for subview in tzStackView.arrangedSubviews { 58 | print("\(subview):") 59 | printConstraints(nonContentSizeLayoutConstraints(subview)) 60 | } 61 | } 62 | 63 | // To override in subclass 64 | func createTestViews() -> [UIView] { 65 | fatalError("implement createViews()") 66 | } 67 | 68 | // To override in subclass 69 | func configureStackViews(_ uiStackView: UIStackView, _ tzStackView: TZStackView) { 70 | } 71 | 72 | func verifyConstraints(log: Bool = false) { 73 | // Force constraints to be created 74 | uiStackView.layoutIfNeeded() 75 | tzStackView.layoutIfNeeded() 76 | 77 | if log { 78 | logAllConstraints() 79 | } 80 | // Assert same constraints are created 81 | assertSameConstraints(uiStackView.constraints, tzStackView.constraints) 82 | 83 | for (index, uiArrangedSubview) in uiStackView.arrangedSubviews.enumerated() { 84 | let tzArrangedSubview = tzStackView.arrangedSubviews[index] 85 | 86 | let uiConstraints = nonContentSizeLayoutConstraints(uiArrangedSubview) 87 | let tzConstraints = nonContentSizeLayoutConstraints(tzArrangedSubview) 88 | 89 | // Assert same constraints are created 90 | assertSameConstraints(uiConstraints, tzConstraints) 91 | } 92 | } 93 | 94 | fileprivate func nonContentSizeLayoutConstraints(_ view: UIView) -> [NSLayoutConstraint] { 95 | return view.constraints.filter({ "\(type(of: $0))" != "NSContentSizeLayoutConstraint" }) 96 | } 97 | 98 | func assertSameConstraints(_ uiConstraints: [NSLayoutConstraint], _ tzConstraints: [NSLayoutConstraint]) { 99 | XCTAssertEqual(uiConstraints.count, tzConstraints.count, "Number of constraints") 100 | if uiConstraints.count != tzConstraints.count { 101 | return 102 | } 103 | 104 | for (index, uiConstraint) in uiConstraints.enumerated() { 105 | let tzConstraint = tzConstraints[index] 106 | 107 | let result = hasSameConstraintMetaData(uiConstraint, tzConstraint) 108 | && (isSameConstraint(uiConstraint, tzConstraint) || isSameConstraintFlipped(uiConstraint, tzConstraint)) 109 | 110 | XCTAssertTrue(result, "Constraints at index \(index) do not match\n== EXPECTED ==\n\(uiConstraint.readableString())\n\n== ACTUAL ==\n\(tzConstraint.readableString())\n\n") 111 | } 112 | } 113 | 114 | fileprivate func hasSameConstraintMetaData(_ layoutConstraint1: NSLayoutConstraint, _ layoutConstraint2: NSLayoutConstraint) -> Bool { 115 | if layoutConstraint1.constant != layoutConstraint2.constant { 116 | return false 117 | } 118 | if layoutConstraint1.multiplier != layoutConstraint2.multiplier { 119 | return false 120 | } 121 | if layoutConstraint1.priority != layoutConstraint2.priority { 122 | if layoutConstraint1.priority < 100 || layoutConstraint1.priority > 150 123 | || layoutConstraint2.priority < 100 || layoutConstraint2.priority > 150 { 124 | return false 125 | } 126 | } 127 | return true 128 | } 129 | 130 | fileprivate func isSameConstraint(_ layoutConstraint1: NSLayoutConstraint, _ layoutConstraint2: NSLayoutConstraint) -> Bool { 131 | if !viewsEqual(layoutConstraint1.firstItem, layoutConstraint2.firstItem) { 132 | return false 133 | } 134 | if !viewsEqual(layoutConstraint1.secondItem, layoutConstraint2.secondItem) { 135 | return false 136 | } 137 | if layoutConstraint1.firstAttribute != layoutConstraint2.firstAttribute { 138 | return false 139 | } 140 | if layoutConstraint1.secondAttribute != layoutConstraint2.secondAttribute { 141 | return false 142 | } 143 | return true 144 | } 145 | 146 | fileprivate func isSameConstraintFlipped(_ layoutConstraint1: NSLayoutConstraint, _ layoutConstraint2: NSLayoutConstraint) -> Bool { 147 | if !viewsEqual(layoutConstraint1.firstItem, layoutConstraint2.secondItem) { 148 | return false 149 | } 150 | if !viewsEqual(layoutConstraint1.secondItem, layoutConstraint2.firstItem) { 151 | return false 152 | } 153 | if layoutConstraint1.firstAttribute != layoutConstraint2.secondAttribute { 154 | return false 155 | } 156 | if layoutConstraint1.secondAttribute != layoutConstraint2.firstAttribute { 157 | return false 158 | } 159 | return true 160 | } 161 | 162 | fileprivate func printConstraints(_ constraints: [NSLayoutConstraint]) { 163 | for constraint in constraints { 164 | print(constraint.readableString()) 165 | } 166 | } 167 | 168 | fileprivate func viewsEqual(_ object1: AnyObject?, _ object2: AnyObject?) -> Bool { 169 | if object1 == nil && object2 == nil { 170 | return true 171 | } 172 | if let view1 = object1 as? UIView, let view2 = object2 as? UIView , view1 == view2 { 173 | return true 174 | } 175 | if object1 is UIStackView && object2 is TZStackView { 176 | return true 177 | } 178 | if object1 is TZStackView && object2 is UIStackView { 179 | return true 180 | } 181 | // Wish I could assert more accurately than this 182 | if object1 is UILayoutGuide && object2 is TZSpacerView { 183 | return true 184 | } 185 | // Wish I could assert more accurately than this 186 | if object1 is TZSpacerView && object2 is UILayoutGuide { 187 | return true 188 | } 189 | return false 190 | } 191 | 192 | func assertSameOrder(_ uiTestViews: [TestView], _ tzTestViews: [TestView]) { 193 | for (index, uiTestView) in uiTestViews.enumerated() { 194 | let tzTestView = tzTestViews[index] 195 | 196 | let result = uiTestView.index == tzTestView.index 197 | 198 | XCTAssertTrue(result, "Views at index \(index) do not match\n== EXPECTED ==\n\(uiTestView.description)\n\n== ACTUAL ==\n\(tzTestView.description)\n\n") 199 | } 200 | } 201 | 202 | func verifyArrangedSubviewConsistency() { 203 | XCTAssertEqual(uiStackView.arrangedSubviews.count, tzStackView.arrangedSubviews.count, "Number of arranged subviews") 204 | 205 | let uiArrangedSubviews = uiStackView.arrangedSubviews as! [TestView] 206 | let tzArrangedSubviews = tzStackView.arrangedSubviews as! [TestView] 207 | 208 | assertSameOrder(uiArrangedSubviews, tzArrangedSubviews) 209 | 210 | for tzTestView in tzArrangedSubviews { 211 | let result = tzStackView.subviews.contains(tzTestView) 212 | 213 | XCTAssertTrue(result, "\(tzTestView.description) is in arranged subviews but is not actually a subview") 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /TZStackViewTests/TZStackViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TZStackViewTests.swift 3 | // TZStackViewTests 4 | // 5 | // Created by Tom van Zummeren on 10/06/15. 6 | // Copyright (c) 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | import TZStackView 13 | 14 | class TZStackViewTests: TZStackViewTestCase { 15 | 16 | override func createTestViews() -> [UIView] { 17 | var views = [UIView]() 18 | for i in 0 ..< 5 { 19 | views.append(TestView(index: i, size: CGSize(width: 100 * (i + 1), height: 100 * (i + 1)))) 20 | } 21 | // views[0].hidden = true 22 | // views[1].hidden = true 23 | // views[2].hidden = true 24 | // views[3].hidden = true 25 | // views[4].hidden = true 26 | return views 27 | } 28 | 29 | // Be careful to configure the same thing on the UIStackView as on the TZStackView, otherwise the test is unfair 30 | override func configureStackViews(_ uiStackView: UIStackView, _ tzStackView: TZStackView) { 31 | uiStackView.isLayoutMarginsRelativeArrangement = false 32 | tzStackView.layoutMarginsRelativeArrangement = false 33 | } 34 | 35 | func testFillHorizontalFillEqually() { 36 | uiStackView.axis = .horizontal 37 | uiStackView.distribution = .fillEqually 38 | uiStackView.alignment = .fill 39 | tzStackView.axis = .Horizontal 40 | tzStackView.distribution = .FillEqually 41 | tzStackView.alignment = .Fill 42 | 43 | uiStackView.spacing = 10 44 | tzStackView.spacing = 10 45 | 46 | verifyConstraints() 47 | } 48 | 49 | func testFillVerticalFillEqually() { 50 | uiStackView.axis = .vertical 51 | uiStackView.distribution = .fillEqually 52 | uiStackView.alignment = .fill 53 | tzStackView.axis = .Vertical 54 | tzStackView.distribution = .FillEqually 55 | tzStackView.alignment = .Fill 56 | 57 | uiStackView.spacing = 10 58 | tzStackView.spacing = 10 59 | 60 | verifyConstraints() 61 | } 62 | 63 | func testFillHorizontalFill() { 64 | uiStackView.axis = .horizontal 65 | uiStackView.distribution = .fill 66 | uiStackView.alignment = .fill 67 | tzStackView.axis = .Horizontal 68 | tzStackView.distribution = .Fill 69 | tzStackView.alignment = .Fill 70 | 71 | uiStackView.spacing = 10 72 | tzStackView.spacing = 10 73 | 74 | verifyConstraints() 75 | } 76 | 77 | func testFillVerticalFill() { 78 | uiStackView.axis = .vertical 79 | uiStackView.distribution = .fill 80 | uiStackView.alignment = .fill 81 | tzStackView.axis = .Vertical 82 | tzStackView.distribution = .Fill 83 | tzStackView.alignment = .Fill 84 | 85 | uiStackView.spacing = 10 86 | tzStackView.spacing = 10 87 | 88 | verifyConstraints() 89 | } 90 | 91 | func testFillHorizontalFillProportionally() { 92 | uiStackView.axis = .horizontal 93 | uiStackView.distribution = .fillProportionally 94 | uiStackView.alignment = .fill 95 | tzStackView.axis = .Horizontal 96 | tzStackView.distribution = .FillProportionally 97 | tzStackView.alignment = .Fill 98 | 99 | uiStackView.spacing = 10 100 | tzStackView.spacing = 10 101 | 102 | verifyConstraints() 103 | } 104 | 105 | func testFillVerticalFillProportionally() { 106 | uiStackView.axis = .vertical 107 | uiStackView.distribution = .fillProportionally 108 | uiStackView.alignment = .fill 109 | tzStackView.axis = .Vertical 110 | tzStackView.distribution = .FillProportionally 111 | tzStackView.alignment = .Fill 112 | 113 | uiStackView.spacing = 10 114 | tzStackView.spacing = 10 115 | 116 | verifyConstraints() 117 | } 118 | 119 | func testFillHorizontalEqualSpacing() { 120 | uiStackView.axis = .horizontal 121 | uiStackView.distribution = .equalSpacing 122 | uiStackView.alignment = .fill 123 | tzStackView.axis = .Horizontal 124 | tzStackView.distribution = .EqualSpacing 125 | tzStackView.alignment = .Fill 126 | 127 | uiStackView.spacing = 10 128 | tzStackView.spacing = 10 129 | 130 | verifyConstraints() 131 | } 132 | 133 | func testFillVerticalEqualSpacing() { 134 | uiStackView.axis = .vertical 135 | uiStackView.distribution = .equalSpacing 136 | uiStackView.alignment = .fill 137 | tzStackView.axis = .Vertical 138 | tzStackView.distribution = .EqualSpacing 139 | tzStackView.alignment = .Fill 140 | 141 | uiStackView.spacing = 10 142 | tzStackView.spacing = 10 143 | 144 | verifyConstraints() 145 | } 146 | 147 | func testFillHorizontalEqualCentering() { 148 | uiStackView.axis = .horizontal 149 | uiStackView.distribution = .equalCentering 150 | uiStackView.alignment = .fill 151 | tzStackView.axis = .Horizontal 152 | tzStackView.distribution = .EqualCentering 153 | tzStackView.alignment = .Fill 154 | 155 | uiStackView.spacing = 10 156 | tzStackView.spacing = 10 157 | 158 | verifyConstraints() 159 | } 160 | 161 | func testFillVerticalEqualCentering() { 162 | uiStackView.axis = .vertical 163 | uiStackView.distribution = .equalCentering 164 | uiStackView.alignment = .fill 165 | tzStackView.axis = .Vertical 166 | tzStackView.distribution = .EqualCentering 167 | tzStackView.alignment = .Fill 168 | 169 | uiStackView.spacing = 10 170 | tzStackView.spacing = 10 171 | 172 | verifyConstraints() 173 | } 174 | 175 | func testCenterHorizontalFillEqually() { 176 | uiStackView.axis = .horizontal 177 | uiStackView.distribution = .fillEqually 178 | uiStackView.alignment = .center 179 | tzStackView.axis = .Horizontal 180 | tzStackView.distribution = .FillEqually 181 | tzStackView.alignment = .Center 182 | 183 | uiStackView.spacing = 10 184 | tzStackView.spacing = 10 185 | 186 | verifyConstraints() 187 | } 188 | 189 | func testCenterVerticalFillEqually() { 190 | uiStackView.axis = .vertical 191 | uiStackView.distribution = .fillEqually 192 | uiStackView.alignment = .center 193 | tzStackView.axis = .Vertical 194 | tzStackView.distribution = .FillEqually 195 | tzStackView.alignment = .Center 196 | 197 | uiStackView.spacing = 10 198 | tzStackView.spacing = 10 199 | 200 | verifyConstraints() 201 | } 202 | 203 | func testCenterHorizontalFill() { 204 | uiStackView.axis = .horizontal 205 | uiStackView.distribution = .fill 206 | uiStackView.alignment = .center 207 | tzStackView.axis = .Horizontal 208 | tzStackView.distribution = .Fill 209 | tzStackView.alignment = .Center 210 | 211 | uiStackView.spacing = 10 212 | tzStackView.spacing = 10 213 | 214 | verifyConstraints() 215 | } 216 | 217 | func testCenterVerticalFill() { 218 | uiStackView.axis = .vertical 219 | uiStackView.distribution = .fill 220 | uiStackView.alignment = .center 221 | tzStackView.axis = .Vertical 222 | tzStackView.distribution = .Fill 223 | tzStackView.alignment = .Center 224 | 225 | uiStackView.spacing = 10 226 | tzStackView.spacing = 10 227 | 228 | verifyConstraints() 229 | } 230 | 231 | func testCenterHorizontalFillProportionally() { 232 | uiStackView.axis = .horizontal 233 | uiStackView.distribution = .fillProportionally 234 | uiStackView.alignment = .center 235 | tzStackView.axis = .Horizontal 236 | tzStackView.distribution = .FillProportionally 237 | tzStackView.alignment = .Center 238 | 239 | uiStackView.spacing = 10 240 | tzStackView.spacing = 10 241 | 242 | verifyConstraints() 243 | } 244 | 245 | func testCenterVetricalFillProportionally() { 246 | uiStackView.axis = .vertical 247 | uiStackView.distribution = .fillProportionally 248 | uiStackView.alignment = .center 249 | tzStackView.axis = .Vertical 250 | tzStackView.distribution = .FillProportionally 251 | tzStackView.alignment = .Center 252 | 253 | uiStackView.spacing = 10 254 | tzStackView.spacing = 10 255 | 256 | verifyConstraints() 257 | } 258 | 259 | func testCenterHorizontalEqualSpacing() { 260 | uiStackView.axis = .horizontal 261 | uiStackView.distribution = .equalSpacing 262 | uiStackView.alignment = .center 263 | tzStackView.axis = .Horizontal 264 | tzStackView.distribution = .EqualSpacing 265 | tzStackView.alignment = .Center 266 | 267 | uiStackView.spacing = 10 268 | tzStackView.spacing = 10 269 | 270 | verifyConstraints() 271 | } 272 | 273 | func testCenterVerticalEqualSpacing() { 274 | uiStackView.axis = .vertical 275 | uiStackView.distribution = .equalSpacing 276 | uiStackView.alignment = .center 277 | tzStackView.axis = .Vertical 278 | tzStackView.distribution = .EqualSpacing 279 | tzStackView.alignment = .Center 280 | 281 | uiStackView.spacing = 10 282 | tzStackView.spacing = 10 283 | 284 | verifyConstraints() 285 | } 286 | 287 | func testCenterHorizontalEqualCentering() { 288 | uiStackView.axis = .horizontal 289 | uiStackView.distribution = .equalCentering 290 | uiStackView.alignment = .center 291 | tzStackView.axis = .Horizontal 292 | tzStackView.distribution = .EqualCentering 293 | tzStackView.alignment = .Center 294 | 295 | uiStackView.spacing = 10 296 | tzStackView.spacing = 10 297 | 298 | verifyConstraints() 299 | } 300 | 301 | func testCenterVerticalEqualCentering() { 302 | uiStackView.axis = .vertical 303 | uiStackView.distribution = .equalCentering 304 | uiStackView.alignment = .center 305 | tzStackView.axis = .Vertical 306 | tzStackView.distribution = .EqualCentering 307 | tzStackView.alignment = .Center 308 | 309 | uiStackView.spacing = 10 310 | tzStackView.spacing = 10 311 | 312 | verifyConstraints() 313 | } 314 | 315 | func testLeadingHorizontalFillEqually() { 316 | uiStackView.axis = .horizontal 317 | uiStackView.distribution = .fillEqually 318 | uiStackView.alignment = .leading 319 | tzStackView.axis = .Horizontal 320 | tzStackView.distribution = .FillEqually 321 | tzStackView.alignment = .Leading 322 | 323 | uiStackView.spacing = 10 324 | tzStackView.spacing = 10 325 | 326 | verifyConstraints() 327 | } 328 | 329 | func testLeadingHorizontalFill() { 330 | uiStackView.axis = .horizontal 331 | uiStackView.distribution = .fill 332 | uiStackView.alignment = .leading 333 | tzStackView.axis = .Horizontal 334 | tzStackView.distribution = .Fill 335 | tzStackView.alignment = .Leading 336 | 337 | uiStackView.spacing = 10 338 | tzStackView.spacing = 10 339 | 340 | verifyConstraints() 341 | } 342 | 343 | func testLeadingHorizontalFillProportionally() { 344 | uiStackView.axis = .horizontal 345 | uiStackView.distribution = .fillProportionally 346 | uiStackView.alignment = .leading 347 | tzStackView.axis = .Horizontal 348 | tzStackView.distribution = .FillProportionally 349 | tzStackView.alignment = .Leading 350 | 351 | uiStackView.spacing = 10 352 | tzStackView.spacing = 10 353 | 354 | verifyConstraints() 355 | } 356 | 357 | func testLeadingHorizontalEqualSpacing() { 358 | uiStackView.axis = .horizontal 359 | uiStackView.distribution = .equalSpacing 360 | uiStackView.alignment = .leading 361 | tzStackView.axis = .Horizontal 362 | tzStackView.distribution = .EqualSpacing 363 | tzStackView.alignment = .Leading 364 | 365 | uiStackView.spacing = 10 366 | tzStackView.spacing = 10 367 | 368 | verifyConstraints() 369 | } 370 | 371 | func testLeadingHorizontalEqualCentering() { 372 | uiStackView.axis = .horizontal 373 | uiStackView.distribution = .equalCentering 374 | uiStackView.alignment = .leading 375 | tzStackView.axis = .Horizontal 376 | tzStackView.distribution = .EqualCentering 377 | tzStackView.alignment = .Leading 378 | 379 | uiStackView.spacing = 10 380 | tzStackView.spacing = 10 381 | 382 | verifyConstraints() 383 | } 384 | 385 | func testLeadingVerticalFillEqually() { 386 | uiStackView.axis = .vertical 387 | uiStackView.distribution = .fillEqually 388 | uiStackView.alignment = .leading 389 | tzStackView.axis = .Vertical 390 | tzStackView.distribution = .FillEqually 391 | tzStackView.alignment = .Leading 392 | 393 | uiStackView.spacing = 10 394 | tzStackView.spacing = 10 395 | 396 | verifyConstraints() 397 | } 398 | 399 | func testLeadingVerticalFill() { 400 | uiStackView.axis = .vertical 401 | uiStackView.distribution = .fill 402 | uiStackView.alignment = .leading 403 | tzStackView.axis = .Vertical 404 | tzStackView.distribution = .Fill 405 | tzStackView.alignment = .Leading 406 | 407 | uiStackView.spacing = 10 408 | tzStackView.spacing = 10 409 | 410 | verifyConstraints() 411 | } 412 | 413 | func testLeadingVerticalFillProportionally() { 414 | uiStackView.axis = .vertical 415 | uiStackView.distribution = .fillProportionally 416 | uiStackView.alignment = .leading 417 | tzStackView.axis = .Vertical 418 | tzStackView.distribution = .FillProportionally 419 | tzStackView.alignment = .Leading 420 | 421 | uiStackView.spacing = 10 422 | tzStackView.spacing = 10 423 | 424 | verifyConstraints() 425 | } 426 | 427 | func testLeadingVerticalEqualSpacing() { 428 | uiStackView.axis = .vertical 429 | uiStackView.distribution = .equalSpacing 430 | uiStackView.alignment = .leading 431 | tzStackView.axis = .Vertical 432 | tzStackView.distribution = .EqualSpacing 433 | tzStackView.alignment = .Leading 434 | 435 | uiStackView.spacing = 10 436 | tzStackView.spacing = 10 437 | 438 | verifyConstraints() 439 | } 440 | 441 | func testLeadingVerticalEqualCentering() { 442 | uiStackView.axis = .vertical 443 | uiStackView.distribution = .equalCentering 444 | uiStackView.alignment = .leading 445 | tzStackView.axis = .Vertical 446 | tzStackView.distribution = .EqualCentering 447 | tzStackView.alignment = .Leading 448 | 449 | uiStackView.spacing = 10 450 | tzStackView.spacing = 10 451 | 452 | verifyConstraints() 453 | } 454 | 455 | func testTopHorizontalFillEqually() { 456 | uiStackView.axis = .horizontal 457 | uiStackView.distribution = .fillEqually 458 | uiStackView.alignment = .top 459 | tzStackView.axis = .Horizontal 460 | tzStackView.distribution = .FillEqually 461 | tzStackView.alignment = .Top 462 | 463 | uiStackView.spacing = 10 464 | tzStackView.spacing = 10 465 | 466 | verifyConstraints() 467 | } 468 | 469 | func testTopHorizontalFill() { 470 | uiStackView.axis = .horizontal 471 | uiStackView.distribution = .fill 472 | uiStackView.alignment = .top 473 | tzStackView.axis = .Horizontal 474 | tzStackView.distribution = .Fill 475 | tzStackView.alignment = .Top 476 | 477 | uiStackView.spacing = 10 478 | tzStackView.spacing = 10 479 | 480 | verifyConstraints() 481 | } 482 | 483 | func testTopHorizontalFillProportionally() { 484 | uiStackView.axis = .horizontal 485 | uiStackView.distribution = .fillProportionally 486 | uiStackView.alignment = .top 487 | tzStackView.axis = .Horizontal 488 | tzStackView.distribution = .FillProportionally 489 | tzStackView.alignment = .Top 490 | 491 | uiStackView.spacing = 10 492 | tzStackView.spacing = 10 493 | 494 | verifyConstraints() 495 | } 496 | 497 | func testTopHorizontalEqualSpacing() { 498 | uiStackView.axis = .horizontal 499 | uiStackView.distribution = .equalSpacing 500 | uiStackView.alignment = .top 501 | tzStackView.axis = .Horizontal 502 | tzStackView.distribution = .EqualSpacing 503 | tzStackView.alignment = .Top 504 | 505 | uiStackView.spacing = 10 506 | tzStackView.spacing = 10 507 | 508 | verifyConstraints() 509 | } 510 | 511 | func testTopHorizontalEqualCentering() { 512 | uiStackView.axis = .horizontal 513 | uiStackView.distribution = .equalCentering 514 | uiStackView.alignment = .top 515 | tzStackView.axis = .Horizontal 516 | tzStackView.distribution = .EqualCentering 517 | tzStackView.alignment = .Top 518 | 519 | uiStackView.spacing = 10 520 | tzStackView.spacing = 10 521 | 522 | verifyConstraints() 523 | } 524 | 525 | func testTopHVerticalFillEqually() { 526 | uiStackView.axis = .vertical 527 | uiStackView.distribution = .fillEqually 528 | uiStackView.alignment = .top 529 | tzStackView.axis = .Vertical 530 | tzStackView.distribution = .FillEqually 531 | tzStackView.alignment = .Top 532 | 533 | uiStackView.spacing = 10 534 | tzStackView.spacing = 10 535 | 536 | verifyConstraints() 537 | } 538 | 539 | func testTopVerticalFill() { 540 | uiStackView.axis = .vertical 541 | uiStackView.distribution = .fill 542 | uiStackView.alignment = .top 543 | tzStackView.axis = .Vertical 544 | tzStackView.distribution = .Fill 545 | tzStackView.alignment = .Top 546 | 547 | uiStackView.spacing = 10 548 | tzStackView.spacing = 10 549 | 550 | verifyConstraints() 551 | } 552 | 553 | func testTopVerticalFillProportionally() { 554 | uiStackView.axis = .vertical 555 | uiStackView.distribution = .fillProportionally 556 | uiStackView.alignment = .top 557 | tzStackView.axis = .Vertical 558 | tzStackView.distribution = .FillProportionally 559 | tzStackView.alignment = .Top 560 | 561 | uiStackView.spacing = 10 562 | tzStackView.spacing = 10 563 | 564 | verifyConstraints() 565 | } 566 | 567 | func testTopVerticalEqualSpacing() { 568 | uiStackView.axis = .vertical 569 | uiStackView.distribution = .equalSpacing 570 | uiStackView.alignment = .top 571 | tzStackView.axis = .Vertical 572 | tzStackView.distribution = .EqualSpacing 573 | tzStackView.alignment = .Top 574 | 575 | uiStackView.spacing = 10 576 | tzStackView.spacing = 10 577 | 578 | verifyConstraints() 579 | } 580 | 581 | func testTopVerticalEqualCentering() { 582 | uiStackView.axis = .vertical 583 | uiStackView.distribution = .equalCentering 584 | uiStackView.alignment = .top 585 | tzStackView.axis = .Vertical 586 | tzStackView.distribution = .EqualCentering 587 | tzStackView.alignment = .Top 588 | 589 | uiStackView.spacing = 10 590 | tzStackView.spacing = 10 591 | 592 | verifyConstraints() 593 | } 594 | 595 | func testTrailingHorizontalFillEqually() { 596 | uiStackView.axis = .horizontal 597 | uiStackView.distribution = .fillEqually 598 | uiStackView.alignment = .trailing 599 | tzStackView.axis = .Horizontal 600 | tzStackView.distribution = .FillEqually 601 | tzStackView.alignment = .Trailing 602 | 603 | uiStackView.spacing = 10 604 | tzStackView.spacing = 10 605 | 606 | verifyConstraints() 607 | } 608 | 609 | func testTrailingHorizontalFill() { 610 | uiStackView.axis = .horizontal 611 | uiStackView.distribution = .fill 612 | uiStackView.alignment = .trailing 613 | tzStackView.axis = .Horizontal 614 | tzStackView.distribution = .Fill 615 | tzStackView.alignment = .Trailing 616 | 617 | uiStackView.spacing = 10 618 | tzStackView.spacing = 10 619 | 620 | verifyConstraints() 621 | } 622 | 623 | func testTrailingHorizontalFillProportionally() { 624 | uiStackView.axis = .horizontal 625 | uiStackView.distribution = .fillProportionally 626 | uiStackView.alignment = .trailing 627 | tzStackView.axis = .Horizontal 628 | tzStackView.distribution = .FillProportionally 629 | tzStackView.alignment = .Trailing 630 | 631 | uiStackView.spacing = 10 632 | tzStackView.spacing = 10 633 | 634 | verifyConstraints() 635 | } 636 | 637 | func testTrailingHorizontalEqualSpacing() { 638 | uiStackView.axis = .horizontal 639 | uiStackView.distribution = .equalSpacing 640 | uiStackView.alignment = .trailing 641 | tzStackView.axis = .Horizontal 642 | tzStackView.distribution = .EqualSpacing 643 | tzStackView.alignment = .Trailing 644 | 645 | uiStackView.spacing = 10 646 | tzStackView.spacing = 10 647 | 648 | verifyConstraints() 649 | } 650 | 651 | func testTrailingHorizontalEqualCentering() { 652 | uiStackView.axis = .horizontal 653 | uiStackView.distribution = .equalCentering 654 | uiStackView.alignment = .trailing 655 | tzStackView.axis = .Horizontal 656 | tzStackView.distribution = .EqualCentering 657 | tzStackView.alignment = .Trailing 658 | 659 | uiStackView.spacing = 10 660 | tzStackView.spacing = 10 661 | 662 | verifyConstraints() 663 | } 664 | 665 | func testTrailingVerticalFillEqually() { 666 | uiStackView.axis = .vertical 667 | uiStackView.distribution = .fillEqually 668 | uiStackView.alignment = .trailing 669 | tzStackView.axis = .Vertical 670 | tzStackView.distribution = .FillEqually 671 | tzStackView.alignment = .Trailing 672 | 673 | uiStackView.spacing = 10 674 | tzStackView.spacing = 10 675 | 676 | verifyConstraints() 677 | } 678 | 679 | func testTrailingVerticalFill() { 680 | uiStackView.axis = .vertical 681 | uiStackView.distribution = .fill 682 | uiStackView.alignment = .trailing 683 | tzStackView.axis = .Vertical 684 | tzStackView.distribution = .Fill 685 | tzStackView.alignment = .Trailing 686 | 687 | uiStackView.spacing = 10 688 | tzStackView.spacing = 10 689 | 690 | verifyConstraints() 691 | } 692 | 693 | func testTrailingVerticalFillProportionally() { 694 | uiStackView.axis = .vertical 695 | uiStackView.distribution = .fillProportionally 696 | uiStackView.alignment = .trailing 697 | tzStackView.axis = .Vertical 698 | tzStackView.distribution = .FillProportionally 699 | tzStackView.alignment = .Trailing 700 | 701 | uiStackView.spacing = 10 702 | tzStackView.spacing = 10 703 | 704 | verifyConstraints() 705 | } 706 | 707 | func testTrailingVerticalEqualSpacing() { 708 | uiStackView.axis = .vertical 709 | uiStackView.distribution = .equalSpacing 710 | uiStackView.alignment = .trailing 711 | tzStackView.axis = .Vertical 712 | tzStackView.distribution = .EqualSpacing 713 | tzStackView.alignment = .Trailing 714 | 715 | uiStackView.spacing = 10 716 | tzStackView.spacing = 10 717 | 718 | verifyConstraints() 719 | } 720 | 721 | func testTrailingVerticalEqualCentering() { 722 | uiStackView.axis = .vertical 723 | uiStackView.distribution = .equalCentering 724 | uiStackView.alignment = .trailing 725 | tzStackView.axis = .Vertical 726 | tzStackView.distribution = .EqualCentering 727 | tzStackView.alignment = .Trailing 728 | 729 | uiStackView.spacing = 10 730 | tzStackView.spacing = 10 731 | 732 | verifyConstraints() 733 | } 734 | 735 | func testBottomHorizontalFillEqually() { 736 | uiStackView.axis = .horizontal 737 | uiStackView.distribution = .fillEqually 738 | uiStackView.alignment = .bottom 739 | tzStackView.axis = .Horizontal 740 | tzStackView.distribution = .FillEqually 741 | tzStackView.alignment = .Bottom 742 | 743 | uiStackView.spacing = 10 744 | tzStackView.spacing = 10 745 | 746 | verifyConstraints() 747 | } 748 | 749 | func testBottomHorizontalFill() { 750 | uiStackView.axis = .horizontal 751 | uiStackView.distribution = .fill 752 | uiStackView.alignment = .bottom 753 | tzStackView.axis = .Horizontal 754 | tzStackView.distribution = .Fill 755 | tzStackView.alignment = .Bottom 756 | 757 | uiStackView.spacing = 10 758 | tzStackView.spacing = 10 759 | 760 | verifyConstraints() 761 | } 762 | 763 | func testBottomHorizontalFillProportionally() { 764 | uiStackView.axis = .horizontal 765 | uiStackView.distribution = .fillProportionally 766 | uiStackView.alignment = .bottom 767 | tzStackView.axis = .Horizontal 768 | tzStackView.distribution = .FillProportionally 769 | tzStackView.alignment = .Bottom 770 | 771 | uiStackView.spacing = 10 772 | tzStackView.spacing = 10 773 | 774 | verifyConstraints() 775 | } 776 | 777 | func testBottomHorizontalEqualSpacing() { 778 | uiStackView.axis = .horizontal 779 | uiStackView.distribution = .equalSpacing 780 | uiStackView.alignment = .bottom 781 | tzStackView.axis = .Horizontal 782 | tzStackView.distribution = .EqualSpacing 783 | tzStackView.alignment = .Bottom 784 | 785 | uiStackView.spacing = 10 786 | tzStackView.spacing = 10 787 | 788 | verifyConstraints() 789 | } 790 | 791 | func testBottomHorizontalEqualCentering() { 792 | uiStackView.axis = .horizontal 793 | uiStackView.distribution = .equalCentering 794 | uiStackView.alignment = .bottom 795 | tzStackView.axis = .Horizontal 796 | tzStackView.distribution = .EqualCentering 797 | tzStackView.alignment = .Bottom 798 | 799 | uiStackView.spacing = 10 800 | tzStackView.spacing = 10 801 | 802 | verifyConstraints() 803 | } 804 | 805 | func testBottomVerticalFillEqually() { 806 | uiStackView.axis = .vertical 807 | uiStackView.distribution = .fillEqually 808 | uiStackView.alignment = .bottom 809 | tzStackView.axis = .Vertical 810 | tzStackView.distribution = .FillEqually 811 | tzStackView.alignment = .Bottom 812 | 813 | uiStackView.spacing = 10 814 | tzStackView.spacing = 10 815 | 816 | verifyConstraints() 817 | } 818 | 819 | func testBottomVerticalFill() { 820 | uiStackView.axis = .vertical 821 | uiStackView.distribution = .fill 822 | uiStackView.alignment = .bottom 823 | tzStackView.axis = .Vertical 824 | tzStackView.distribution = .Fill 825 | tzStackView.alignment = .Bottom 826 | 827 | uiStackView.spacing = 10 828 | tzStackView.spacing = 10 829 | 830 | verifyConstraints() 831 | } 832 | 833 | func testBottomVerticalFillProportionally() { 834 | uiStackView.axis = .vertical 835 | uiStackView.distribution = .fillProportionally 836 | uiStackView.alignment = .bottom 837 | tzStackView.axis = .Vertical 838 | tzStackView.distribution = .FillProportionally 839 | tzStackView.alignment = .Bottom 840 | 841 | uiStackView.spacing = 10 842 | tzStackView.spacing = 10 843 | 844 | verifyConstraints() 845 | } 846 | 847 | func testBottomVerticalEqualSpacing() { 848 | uiStackView.axis = .vertical 849 | uiStackView.distribution = .equalSpacing 850 | uiStackView.alignment = .bottom 851 | tzStackView.axis = .Vertical 852 | tzStackView.distribution = .EqualSpacing 853 | tzStackView.alignment = .Bottom 854 | 855 | uiStackView.spacing = 10 856 | tzStackView.spacing = 10 857 | 858 | verifyConstraints() 859 | } 860 | 861 | func testBottomVerticalEqualCentering() { 862 | uiStackView.axis = .vertical 863 | uiStackView.distribution = .equalCentering 864 | uiStackView.alignment = .bottom 865 | tzStackView.axis = .Vertical 866 | tzStackView.distribution = .EqualCentering 867 | tzStackView.alignment = .Bottom 868 | 869 | uiStackView.spacing = 10 870 | tzStackView.spacing = 10 871 | 872 | verifyConstraints() 873 | } 874 | 875 | func testFirstBaselineHorizontalFillEqually() { 876 | uiStackView.axis = .horizontal 877 | uiStackView.distribution = .fillEqually 878 | uiStackView.alignment = .firstBaseline 879 | tzStackView.axis = .Horizontal 880 | tzStackView.distribution = .FillEqually 881 | tzStackView.alignment = .FirstBaseline 882 | 883 | uiStackView.spacing = 10 884 | tzStackView.spacing = 10 885 | 886 | verifyConstraints() 887 | } 888 | 889 | func testFirstBaselineHorizontalFill() { 890 | uiStackView.axis = .horizontal 891 | uiStackView.distribution = .fill 892 | uiStackView.alignment = .firstBaseline 893 | tzStackView.axis = .Horizontal 894 | tzStackView.distribution = .Fill 895 | tzStackView.alignment = .FirstBaseline 896 | 897 | uiStackView.spacing = 10 898 | tzStackView.spacing = 10 899 | 900 | verifyConstraints() 901 | } 902 | 903 | func testFirstBaselineHorizontalFillProportionally() { 904 | uiStackView.axis = .horizontal 905 | uiStackView.distribution = .fillProportionally 906 | uiStackView.alignment = .firstBaseline 907 | tzStackView.axis = .Horizontal 908 | tzStackView.distribution = .FillProportionally 909 | tzStackView.alignment = .FirstBaseline 910 | 911 | uiStackView.spacing = 10 912 | tzStackView.spacing = 10 913 | 914 | verifyConstraints() 915 | } 916 | 917 | func testFirstBaselineHorizontalEqualSpacing() { 918 | uiStackView.axis = .horizontal 919 | uiStackView.distribution = .equalSpacing 920 | uiStackView.alignment = .firstBaseline 921 | tzStackView.axis = .Horizontal 922 | tzStackView.distribution = .EqualSpacing 923 | tzStackView.alignment = .FirstBaseline 924 | 925 | uiStackView.spacing = 10 926 | tzStackView.spacing = 10 927 | 928 | verifyConstraints() 929 | } 930 | 931 | func testFirstBaselineHorizontalEqualCentering() { 932 | uiStackView.axis = .horizontal 933 | uiStackView.distribution = .equalCentering 934 | uiStackView.alignment = .firstBaseline 935 | tzStackView.axis = .Horizontal 936 | tzStackView.distribution = .EqualCentering 937 | tzStackView.alignment = .FirstBaseline 938 | 939 | uiStackView.spacing = 10 940 | tzStackView.spacing = 10 941 | 942 | verifyConstraints() 943 | } 944 | 945 | func testFirstBaselineVerticalFillEqually() { 946 | uiStackView.axis = .vertical 947 | uiStackView.distribution = .fillEqually 948 | uiStackView.alignment = .firstBaseline 949 | tzStackView.axis = .Vertical 950 | tzStackView.distribution = .FillEqually 951 | tzStackView.alignment = .FirstBaseline 952 | 953 | uiStackView.spacing = 10 954 | tzStackView.spacing = 10 955 | 956 | verifyConstraints() 957 | logAllConstraints() 958 | } 959 | 960 | func testFirstBaselineVerticalFill() { 961 | uiStackView.axis = .vertical 962 | uiStackView.distribution = .fill 963 | uiStackView.alignment = .firstBaseline 964 | tzStackView.axis = .Vertical 965 | tzStackView.distribution = .Fill 966 | tzStackView.alignment = .FirstBaseline 967 | 968 | uiStackView.spacing = 10 969 | tzStackView.spacing = 10 970 | 971 | verifyConstraints() 972 | } 973 | 974 | func testFirstBaselineVerticalFillProportionally() { 975 | uiStackView.axis = .vertical 976 | uiStackView.distribution = .fillProportionally 977 | uiStackView.alignment = .firstBaseline 978 | tzStackView.axis = .Vertical 979 | tzStackView.distribution = .FillProportionally 980 | tzStackView.alignment = .FirstBaseline 981 | 982 | uiStackView.spacing = 10 983 | tzStackView.spacing = 10 984 | 985 | verifyConstraints() 986 | } 987 | 988 | func testFirstBaselineVerticalEqualSpacing() { 989 | uiStackView.axis = .vertical 990 | uiStackView.distribution = .equalSpacing 991 | uiStackView.alignment = .firstBaseline 992 | tzStackView.axis = .Vertical 993 | tzStackView.distribution = .EqualSpacing 994 | tzStackView.alignment = .FirstBaseline 995 | 996 | uiStackView.spacing = 10 997 | tzStackView.spacing = 10 998 | 999 | verifyConstraints() 1000 | } 1001 | 1002 | func testFirstBaselineVerticalEqualCentering() { 1003 | uiStackView.axis = .vertical 1004 | uiStackView.distribution = .equalCentering 1005 | uiStackView.alignment = .firstBaseline 1006 | tzStackView.axis = .Vertical 1007 | tzStackView.distribution = .EqualCentering 1008 | tzStackView.alignment = .FirstBaseline 1009 | 1010 | uiStackView.spacing = 10 1011 | tzStackView.spacing = 10 1012 | 1013 | verifyConstraints() 1014 | } 1015 | 1016 | // MARK: - Maintaining Consistency Between the Arranged Views and Subviews 1017 | // https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/#//apple_ref/doc/uid/TP40015256-CH1-SW29 1018 | func testConsistencyWhenAddingArrangedSubview() { 1019 | let uiTestView = TestView(index: -1, size: CGSize(width: 100, height: 100)) 1020 | uiStackView.addArrangedSubview(uiTestView) 1021 | 1022 | let tzTestView = TestView(index: -1, size: CGSize(width: 100, height: 100)) 1023 | tzStackView.addArrangedSubview(tzTestView) 1024 | 1025 | verifyArrangedSubviewConsistency() 1026 | } 1027 | 1028 | func testConsistencyWhenInsertingArrangedSubview() { 1029 | let uiTestView = TestView(index: -1, size: CGSize(width: 100, height: 100)) 1030 | uiStackView.insertArrangedSubview(uiTestView, at: 0) 1031 | 1032 | let tzTestView = TestView(index: -1, size: CGSize(width: 100, height: 100)) 1033 | tzStackView.insertArrangedSubview(tzTestView, atIndex: 0) 1034 | 1035 | verifyArrangedSubviewConsistency() 1036 | } 1037 | 1038 | func testConsistencyWhenRemovingArrangedSubview() { 1039 | let uiTestView = uiStackView.arrangedSubviews.last 1040 | uiStackView.removeArrangedSubview(uiTestView!) 1041 | 1042 | let tzTestView = tzStackView.arrangedSubviews.last 1043 | tzStackView.removeArrangedSubview(tzTestView!) 1044 | 1045 | verifyArrangedSubviewConsistency() 1046 | } 1047 | 1048 | func testConsistencyWhenRemovingSubview() { 1049 | let uiTestView = uiStackView.arrangedSubviews.last 1050 | uiTestView!.removeFromSuperview() 1051 | 1052 | let tzTestView = tzStackView.arrangedSubviews.last 1053 | tzTestView!.removeFromSuperview() 1054 | 1055 | verifyArrangedSubviewConsistency() 1056 | } 1057 | } 1058 | -------------------------------------------------------------------------------- /TZStackViewTests/TestView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestView.swift 3 | // TZStackView 4 | // 5 | // Created by Tom van Zummeren on 11/06/15. 6 | // Copyright © 2015 Tom van Zummeren. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestView: UIView { 12 | 13 | let index: Int 14 | let size: CGSize 15 | 16 | init(index: Int, size: CGSize) { 17 | self.index = index 18 | self.size = size 19 | super.init(frame: CGRect.zero) 20 | } 21 | 22 | required init(coder aDecoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | override var description: String { 27 | return "TestView\(index)" 28 | } 29 | 30 | override var intrinsicContentSize : CGSize { 31 | return size 32 | } 33 | } 34 | 35 | func ==(lhs: NSObject, rhs: NSObject) -> Bool { 36 | if let lhs = lhs as? TestView, let rhs = rhs as? TestView { 37 | return lhs.index == rhs.index 38 | } 39 | return lhs === rhs 40 | } 41 | -------------------------------------------------------------------------------- /assets/TZStackView-hide-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomvanzummeren/TZStackView/b5bf459c66bb271311be38c4a31a1b1cc2822b9b/assets/TZStackView-hide-animation.gif -------------------------------------------------------------------------------- /assets/layout-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomvanzummeren/TZStackView/b5bf459c66bb271311be38c4a31a1b1cc2822b9b/assets/layout-example.png --------------------------------------------------------------------------------