├── .gitignore ├── .travis.yml ├── AloeStackView.podspec ├── AloeStackView.xcodeproj ├── AloeStackViewTests_Info.plist ├── AloeStackView_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── AloeStackView.xcscheme ├── Contributing.md ├── Docs └── Images │ ├── add_many_rows.gif │ ├── add_rows.png │ ├── dynamically_adjust_content.gif │ ├── example_app.gif │ ├── hide_last_separator.png │ ├── hide_separators_by_default.png │ ├── large_blue_separators.png │ ├── tap_handler.gif │ ├── tappable_protocol.gif │ └── zero_separator_inset.png ├── Example ├── AloeStackViewExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── AloeStackViewExample.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── AloeStackViewExample │ ├── Assets │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── lobster-dog.imageset │ │ │ ├── Contents.json │ │ │ └── lobster-dog.jpg │ └── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Other │ ├── App │ │ └── AppDelegate.swift │ └── Config │ │ └── Info.plist │ ├── ViewControllers │ ├── MainViewController.swift │ └── PhotoViewController.swift │ └── Views │ ├── ExpandingRowView.swift │ └── SwitchRowView.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── AloeStackView │ ├── AloeStackView.swift │ ├── AloeStackViewController.swift │ ├── Protocols │ ├── Highlightable.swift │ ├── SeparatorHiding.swift │ └── Tappable.swift │ └── Views │ ├── SeparatorView.swift │ └── StackViewCell.swift └── Tests └── AloeStackViewTests └── AloeStackViewTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | # macOS 71 | .DS_Store 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | env: 4 | - SWIFT_VERSION=4.0 5 | - SWIFT_VERSION=4.2 6 | - SWIFT_VERSION=5.0 7 | install: 8 | - bundle install 9 | - brew outdated carthage || brew upgrade carthage 10 | before_script: 11 | - bundle exec pod lib lint --verbose --fail-fast 12 | - carthage build --verbose --no-skip-current 13 | script: 14 | - xcodebuild -project AloeStackView.xcodeproj -scheme AloeStackView -sdk iphonesimulator -destination "platform=iOS Simulator,OS=10.0,name=iPhone 6s" -configuration Debug -PBXBuildsContinueAfterErrors=0 SWIFT_VERSION=$SWIFT_VERSION build test 15 | -------------------------------------------------------------------------------- /AloeStackView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'AloeStackView' 3 | s.version = '1.2.0' 4 | s.license = 'Apache License, Version 2.0' 5 | s.summary = 'A simple class for laying out a collection of views with a convenient API, while leveraging the power of Auto Layout.' 6 | s.homepage = 'https://github.com/marlimox/AloeStackView' 7 | s.authors = 'Marli Oshlack' 8 | s.source = { git: 'https://github.com/marlimox/AloeStackView.git', tag: "v#{s.version}" } 9 | s.swift_version = '5.0' 10 | s.source_files = 'Sources/**/*.{swift,h}' 11 | s.ios.deployment_target = '9.0' 12 | end 13 | -------------------------------------------------------------------------------- /AloeStackView.xcodeproj/AloeStackViewTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /AloeStackView.xcodeproj/AloeStackView_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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.2.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /AloeStackView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A74F6EDB216D5CB50054AA18 /* AloeStackView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A74F6ED1216D5CB50054AA18 /* AloeStackView.framework */; }; 11 | A74F6EF6216D5EFF0054AA18 /* AloeStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EED216D5EFE0054AA18 /* AloeStackView.swift */; }; 12 | A74F6EF7216D5EFF0054AA18 /* AloeStackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EEE216D5EFE0054AA18 /* AloeStackViewController.swift */; }; 13 | A74F6EF8216D5EFF0054AA18 /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EF0216D5EFE0054AA18 /* SeparatorView.swift */; }; 14 | A74F6EF9216D5EFF0054AA18 /* StackViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EF1216D5EFE0054AA18 /* StackViewCell.swift */; }; 15 | A74F6EFA216D5EFF0054AA18 /* Tappable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EF3216D5EFE0054AA18 /* Tappable.swift */; }; 16 | A74F6EFB216D5EFF0054AA18 /* SeparatorHiding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EF4216D5EFE0054AA18 /* SeparatorHiding.swift */; }; 17 | A74F6EFC216D5EFF0054AA18 /* Highlightable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EF5216D5EFE0054AA18 /* Highlightable.swift */; }; 18 | A74F6F00216D5F0E0054AA18 /* AloeStackViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F6EFF216D5F0E0054AA18 /* AloeStackViewTests.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | A74F6EDC216D5CB50054AA18 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = A74F6EC8216D5CB40054AA18 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = A74F6ED0216D5CB50054AA18; 27 | remoteInfo = AloeStackView; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | A74F6ED1216D5CB50054AA18 /* AloeStackView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AloeStackView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | A74F6EDA216D5CB50054AA18 /* AloeStackViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AloeStackViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | A74F6EED216D5EFE0054AA18 /* AloeStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AloeStackView.swift; sourceTree = ""; }; 35 | A74F6EEE216D5EFE0054AA18 /* AloeStackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AloeStackViewController.swift; sourceTree = ""; }; 36 | A74F6EF0216D5EFE0054AA18 /* SeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; }; 37 | A74F6EF1216D5EFE0054AA18 /* StackViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewCell.swift; sourceTree = ""; }; 38 | A74F6EF3216D5EFE0054AA18 /* Tappable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tappable.swift; sourceTree = ""; }; 39 | A74F6EF4216D5EFE0054AA18 /* SeparatorHiding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorHiding.swift; sourceTree = ""; }; 40 | A74F6EF5216D5EFE0054AA18 /* Highlightable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Highlightable.swift; sourceTree = ""; }; 41 | A74F6EFF216D5F0E0054AA18 /* AloeStackViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AloeStackViewTests.swift; sourceTree = ""; }; 42 | A74F6F01216D5F230054AA18 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 43 | A74F6F02216D5F230054AA18 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 44 | A74F6F03216D60710054AA18 /* AloeStackView_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = AloeStackView_Info.plist; path = AloeStackView.xcodeproj/AloeStackView_Info.plist; sourceTree = ""; }; 45 | A74F6F04216D60710054AA18 /* AloeStackViewTests_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = AloeStackViewTests_Info.plist; path = AloeStackView.xcodeproj/AloeStackViewTests_Info.plist; sourceTree = ""; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFrameworksBuildPhase section */ 49 | A74F6ECE216D5CB50054AA18 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | A74F6ED7216D5CB50054AA18 /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | A74F6EDB216D5CB50054AA18 /* AloeStackView.framework in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | A74F6EC7216D5CB40054AA18 = { 68 | isa = PBXGroup; 69 | children = ( 70 | A74F6F02216D5F230054AA18 /* README.md */, 71 | A74F6F01216D5F230054AA18 /* LICENSE */, 72 | A74F6F03216D60710054AA18 /* AloeStackView_Info.plist */, 73 | A74F6F04216D60710054AA18 /* AloeStackViewTests_Info.plist */, 74 | A74F6EEB216D5EFE0054AA18 /* Sources */, 75 | A74F6EFD216D5F0E0054AA18 /* Tests */, 76 | A74F6ED2216D5CB50054AA18 /* Products */, 77 | ); 78 | sourceTree = ""; 79 | }; 80 | A74F6ED2216D5CB50054AA18 /* Products */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | A74F6ED1216D5CB50054AA18 /* AloeStackView.framework */, 84 | A74F6EDA216D5CB50054AA18 /* AloeStackViewTests.xctest */, 85 | ); 86 | name = Products; 87 | sourceTree = ""; 88 | }; 89 | A74F6EEB216D5EFE0054AA18 /* Sources */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | A74F6EEC216D5EFE0054AA18 /* AloeStackView */, 93 | ); 94 | path = Sources; 95 | sourceTree = ""; 96 | }; 97 | A74F6EEC216D5EFE0054AA18 /* AloeStackView */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | A74F6EED216D5EFE0054AA18 /* AloeStackView.swift */, 101 | A74F6EEE216D5EFE0054AA18 /* AloeStackViewController.swift */, 102 | A74F6EEF216D5EFE0054AA18 /* Views */, 103 | A74F6EF2216D5EFE0054AA18 /* Protocols */, 104 | ); 105 | path = AloeStackView; 106 | sourceTree = ""; 107 | }; 108 | A74F6EEF216D5EFE0054AA18 /* Views */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | A74F6EF0216D5EFE0054AA18 /* SeparatorView.swift */, 112 | A74F6EF1216D5EFE0054AA18 /* StackViewCell.swift */, 113 | ); 114 | path = Views; 115 | sourceTree = ""; 116 | }; 117 | A74F6EF2216D5EFE0054AA18 /* Protocols */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | A74F6EF3216D5EFE0054AA18 /* Tappable.swift */, 121 | A74F6EF4216D5EFE0054AA18 /* SeparatorHiding.swift */, 122 | A74F6EF5216D5EFE0054AA18 /* Highlightable.swift */, 123 | ); 124 | path = Protocols; 125 | sourceTree = ""; 126 | }; 127 | A74F6EFD216D5F0E0054AA18 /* Tests */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | A74F6EFE216D5F0E0054AA18 /* AloeStackViewTests */, 131 | ); 132 | path = Tests; 133 | sourceTree = ""; 134 | }; 135 | A74F6EFE216D5F0E0054AA18 /* AloeStackViewTests */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | A74F6EFF216D5F0E0054AA18 /* AloeStackViewTests.swift */, 139 | ); 140 | path = AloeStackViewTests; 141 | sourceTree = ""; 142 | }; 143 | /* End PBXGroup section */ 144 | 145 | /* Begin PBXHeadersBuildPhase section */ 146 | A74F6ECC216D5CB50054AA18 /* Headers */ = { 147 | isa = PBXHeadersBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXHeadersBuildPhase section */ 154 | 155 | /* Begin PBXNativeTarget section */ 156 | A74F6ED0216D5CB50054AA18 /* AloeStackView */ = { 157 | isa = PBXNativeTarget; 158 | buildConfigurationList = A74F6EE5216D5CB50054AA18 /* Build configuration list for PBXNativeTarget "AloeStackView" */; 159 | buildPhases = ( 160 | A74F6ECC216D5CB50054AA18 /* Headers */, 161 | A74F6ECD216D5CB50054AA18 /* Sources */, 162 | A74F6ECE216D5CB50054AA18 /* Frameworks */, 163 | A74F6ECF216D5CB50054AA18 /* Resources */, 164 | ); 165 | buildRules = ( 166 | ); 167 | dependencies = ( 168 | ); 169 | name = AloeStackView; 170 | productName = AloeStackView; 171 | productReference = A74F6ED1216D5CB50054AA18 /* AloeStackView.framework */; 172 | productType = "com.apple.product-type.framework"; 173 | }; 174 | A74F6ED9216D5CB50054AA18 /* AloeStackViewTests */ = { 175 | isa = PBXNativeTarget; 176 | buildConfigurationList = A74F6EE8216D5CB50054AA18 /* Build configuration list for PBXNativeTarget "AloeStackViewTests" */; 177 | buildPhases = ( 178 | A74F6ED6216D5CB50054AA18 /* Sources */, 179 | A74F6ED7216D5CB50054AA18 /* Frameworks */, 180 | A74F6ED8216D5CB50054AA18 /* Resources */, 181 | ); 182 | buildRules = ( 183 | ); 184 | dependencies = ( 185 | A74F6EDD216D5CB50054AA18 /* PBXTargetDependency */, 186 | ); 187 | name = AloeStackViewTests; 188 | productName = AloeStackViewTests; 189 | productReference = A74F6EDA216D5CB50054AA18 /* AloeStackViewTests.xctest */; 190 | productType = "com.apple.product-type.bundle.unit-test"; 191 | }; 192 | /* End PBXNativeTarget section */ 193 | 194 | /* Begin PBXProject section */ 195 | A74F6EC8216D5CB40054AA18 /* Project object */ = { 196 | isa = PBXProject; 197 | attributes = { 198 | LastSwiftUpdateCheck = 1000; 199 | LastUpgradeCheck = 1000; 200 | ORGANIZATIONNAME = ""; 201 | TargetAttributes = { 202 | A74F6ED0216D5CB50054AA18 = { 203 | CreatedOnToolsVersion = 10.0; 204 | }; 205 | A74F6ED9216D5CB50054AA18 = { 206 | CreatedOnToolsVersion = 10.0; 207 | }; 208 | }; 209 | }; 210 | buildConfigurationList = A74F6ECB216D5CB40054AA18 /* Build configuration list for PBXProject "AloeStackView" */; 211 | compatibilityVersion = "Xcode 9.3"; 212 | developmentRegion = en; 213 | hasScannedForEncodings = 0; 214 | knownRegions = ( 215 | en, 216 | Base, 217 | ); 218 | mainGroup = A74F6EC7216D5CB40054AA18; 219 | productRefGroup = A74F6ED2216D5CB50054AA18 /* Products */; 220 | projectDirPath = ""; 221 | projectRoot = ""; 222 | targets = ( 223 | A74F6ED0216D5CB50054AA18 /* AloeStackView */, 224 | A74F6ED9216D5CB50054AA18 /* AloeStackViewTests */, 225 | ); 226 | }; 227 | /* End PBXProject section */ 228 | 229 | /* Begin PBXResourcesBuildPhase section */ 230 | A74F6ECF216D5CB50054AA18 /* Resources */ = { 231 | isa = PBXResourcesBuildPhase; 232 | buildActionMask = 2147483647; 233 | files = ( 234 | ); 235 | runOnlyForDeploymentPostprocessing = 0; 236 | }; 237 | A74F6ED8216D5CB50054AA18 /* Resources */ = { 238 | isa = PBXResourcesBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | /* End PBXResourcesBuildPhase section */ 245 | 246 | /* Begin PBXSourcesBuildPhase section */ 247 | A74F6ECD216D5CB50054AA18 /* Sources */ = { 248 | isa = PBXSourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | A74F6EF8216D5EFF0054AA18 /* SeparatorView.swift in Sources */, 252 | A74F6EF7216D5EFF0054AA18 /* AloeStackViewController.swift in Sources */, 253 | A74F6EF6216D5EFF0054AA18 /* AloeStackView.swift in Sources */, 254 | A74F6EF9216D5EFF0054AA18 /* StackViewCell.swift in Sources */, 255 | A74F6EFC216D5EFF0054AA18 /* Highlightable.swift in Sources */, 256 | A74F6EFB216D5EFF0054AA18 /* SeparatorHiding.swift in Sources */, 257 | A74F6EFA216D5EFF0054AA18 /* Tappable.swift in Sources */, 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | A74F6ED6216D5CB50054AA18 /* Sources */ = { 262 | isa = PBXSourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | A74F6F00216D5F0E0054AA18 /* AloeStackViewTests.swift in Sources */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXSourcesBuildPhase section */ 270 | 271 | /* Begin PBXTargetDependency section */ 272 | A74F6EDD216D5CB50054AA18 /* PBXTargetDependency */ = { 273 | isa = PBXTargetDependency; 274 | target = A74F6ED0216D5CB50054AA18 /* AloeStackView */; 275 | targetProxy = A74F6EDC216D5CB50054AA18 /* PBXContainerItemProxy */; 276 | }; 277 | /* End PBXTargetDependency section */ 278 | 279 | /* Begin XCBuildConfiguration section */ 280 | A74F6EE3216D5CB50054AA18 /* Debug */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | ALWAYS_SEARCH_USER_PATHS = NO; 284 | CLANG_ANALYZER_NONNULL = YES; 285 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 286 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 287 | CLANG_CXX_LIBRARY = "libc++"; 288 | CLANG_ENABLE_MODULES = YES; 289 | CLANG_ENABLE_OBJC_ARC = YES; 290 | CLANG_ENABLE_OBJC_WEAK = YES; 291 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 292 | CLANG_WARN_BOOL_CONVERSION = YES; 293 | CLANG_WARN_COMMA = YES; 294 | CLANG_WARN_CONSTANT_CONVERSION = YES; 295 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 296 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 297 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 298 | CLANG_WARN_EMPTY_BODY = YES; 299 | CLANG_WARN_ENUM_CONVERSION = YES; 300 | CLANG_WARN_INFINITE_RECURSION = YES; 301 | CLANG_WARN_INT_CONVERSION = YES; 302 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 304 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 306 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 307 | CLANG_WARN_STRICT_PROTOTYPES = YES; 308 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 309 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 310 | CLANG_WARN_UNREACHABLE_CODE = YES; 311 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 312 | CODE_SIGN_IDENTITY = "iPhone Developer"; 313 | COPY_PHASE_STRIP = NO; 314 | CURRENT_PROJECT_VERSION = 1; 315 | DEBUG_INFORMATION_FORMAT = dwarf; 316 | ENABLE_STRICT_OBJC_MSGSEND = YES; 317 | ENABLE_TESTABILITY = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu11; 319 | GCC_DYNAMIC_NO_PIC = NO; 320 | GCC_NO_COMMON_BLOCKS = YES; 321 | GCC_OPTIMIZATION_LEVEL = 0; 322 | GCC_PREPROCESSOR_DEFINITIONS = ( 323 | "DEBUG=1", 324 | "$(inherited)", 325 | ); 326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 328 | GCC_WARN_UNDECLARED_SELECTOR = YES; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 333 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 334 | MTL_FAST_MATH = YES; 335 | ONLY_ACTIVE_ARCH = YES; 336 | SDKROOT = iphoneos; 337 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 338 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 339 | VERSIONING_SYSTEM = "apple-generic"; 340 | VERSION_INFO_PREFIX = ""; 341 | }; 342 | name = Debug; 343 | }; 344 | A74F6EE4216D5CB50054AA18 /* Release */ = { 345 | isa = XCBuildConfiguration; 346 | buildSettings = { 347 | ALWAYS_SEARCH_USER_PATHS = NO; 348 | CLANG_ANALYZER_NONNULL = YES; 349 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 350 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 351 | CLANG_CXX_LIBRARY = "libc++"; 352 | CLANG_ENABLE_MODULES = YES; 353 | CLANG_ENABLE_OBJC_ARC = YES; 354 | CLANG_ENABLE_OBJC_WEAK = YES; 355 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 356 | CLANG_WARN_BOOL_CONVERSION = YES; 357 | CLANG_WARN_COMMA = YES; 358 | CLANG_WARN_CONSTANT_CONVERSION = YES; 359 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 360 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 361 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 362 | CLANG_WARN_EMPTY_BODY = YES; 363 | CLANG_WARN_ENUM_CONVERSION = YES; 364 | CLANG_WARN_INFINITE_RECURSION = YES; 365 | CLANG_WARN_INT_CONVERSION = YES; 366 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 367 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 368 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 370 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 371 | CLANG_WARN_STRICT_PROTOTYPES = YES; 372 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 373 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 374 | CLANG_WARN_UNREACHABLE_CODE = YES; 375 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 376 | CODE_SIGN_IDENTITY = "iPhone Developer"; 377 | COPY_PHASE_STRIP = NO; 378 | CURRENT_PROJECT_VERSION = 1; 379 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 380 | ENABLE_NS_ASSERTIONS = NO; 381 | ENABLE_STRICT_OBJC_MSGSEND = YES; 382 | GCC_C_LANGUAGE_STANDARD = gnu11; 383 | GCC_NO_COMMON_BLOCKS = YES; 384 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 385 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 386 | GCC_WARN_UNDECLARED_SELECTOR = YES; 387 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 388 | GCC_WARN_UNUSED_FUNCTION = YES; 389 | GCC_WARN_UNUSED_VARIABLE = YES; 390 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 391 | MTL_ENABLE_DEBUG_INFO = NO; 392 | MTL_FAST_MATH = YES; 393 | SDKROOT = iphoneos; 394 | SWIFT_COMPILATION_MODE = wholemodule; 395 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 396 | VALIDATE_PRODUCT = YES; 397 | VERSIONING_SYSTEM = "apple-generic"; 398 | VERSION_INFO_PREFIX = ""; 399 | }; 400 | name = Release; 401 | }; 402 | A74F6EE6216D5CB50054AA18 /* Debug */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | CODE_SIGN_IDENTITY = ""; 406 | CODE_SIGN_STYLE = Automatic; 407 | DEFINES_MODULE = YES; 408 | DEVELOPMENT_TEAM = 5LL7P8E8RA; 409 | DYLIB_COMPATIBILITY_VERSION = 1; 410 | DYLIB_CURRENT_VERSION = 1; 411 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 412 | INFOPLIST_FILE = AloeStackView.xcodeproj/AloeStackView_Info.plist; 413 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 414 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 415 | LD_RUNPATH_SEARCH_PATHS = ( 416 | "$(inherited)", 417 | "@executable_path/Frameworks", 418 | "@loader_path/Frameworks", 419 | ); 420 | PRODUCT_BUNDLE_IDENTIFIER = com.oshlack.marli.AloeStackView; 421 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 422 | SKIP_INSTALL = YES; 423 | SWIFT_VERSION = 5.0; 424 | TARGETED_DEVICE_FAMILY = "1,2"; 425 | }; 426 | name = Debug; 427 | }; 428 | A74F6EE7216D5CB50054AA18 /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | CODE_SIGN_IDENTITY = ""; 432 | CODE_SIGN_STYLE = Automatic; 433 | DEFINES_MODULE = YES; 434 | DEVELOPMENT_TEAM = 5LL7P8E8RA; 435 | DYLIB_COMPATIBILITY_VERSION = 1; 436 | DYLIB_CURRENT_VERSION = 1; 437 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 438 | INFOPLIST_FILE = AloeStackView.xcodeproj/AloeStackView_Info.plist; 439 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 440 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 441 | LD_RUNPATH_SEARCH_PATHS = ( 442 | "$(inherited)", 443 | "@executable_path/Frameworks", 444 | "@loader_path/Frameworks", 445 | ); 446 | PRODUCT_BUNDLE_IDENTIFIER = com.oshlack.marli.AloeStackView; 447 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 448 | SKIP_INSTALL = YES; 449 | SWIFT_VERSION = 5.0; 450 | TARGETED_DEVICE_FAMILY = "1,2"; 451 | }; 452 | name = Release; 453 | }; 454 | A74F6EE9216D5CB50054AA18 /* Debug */ = { 455 | isa = XCBuildConfiguration; 456 | buildSettings = { 457 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 458 | CODE_SIGN_STYLE = Automatic; 459 | DEVELOPMENT_TEAM = 5LL7P8E8RA; 460 | INFOPLIST_FILE = AloeStackView.xcodeproj/AloeStackViewTests_Info.plist; 461 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 462 | LD_RUNPATH_SEARCH_PATHS = ( 463 | "$(inherited)", 464 | "@executable_path/Frameworks", 465 | "@loader_path/Frameworks", 466 | ); 467 | PRODUCT_BUNDLE_IDENTIFIER = com.oshlack.marli.AloeStackViewTests; 468 | PRODUCT_NAME = "$(TARGET_NAME)"; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Debug; 473 | }; 474 | A74F6EEA216D5CB50054AA18 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 478 | CODE_SIGN_STYLE = Automatic; 479 | DEVELOPMENT_TEAM = 5LL7P8E8RA; 480 | INFOPLIST_FILE = AloeStackView.xcodeproj/AloeStackViewTests_Info.plist; 481 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 482 | LD_RUNPATH_SEARCH_PATHS = ( 483 | "$(inherited)", 484 | "@executable_path/Frameworks", 485 | "@loader_path/Frameworks", 486 | ); 487 | PRODUCT_BUNDLE_IDENTIFIER = com.oshlack.marli.AloeStackViewTests; 488 | PRODUCT_NAME = "$(TARGET_NAME)"; 489 | SWIFT_VERSION = 5.0; 490 | TARGETED_DEVICE_FAMILY = "1,2"; 491 | }; 492 | name = Release; 493 | }; 494 | /* End XCBuildConfiguration section */ 495 | 496 | /* Begin XCConfigurationList section */ 497 | A74F6ECB216D5CB40054AA18 /* Build configuration list for PBXProject "AloeStackView" */ = { 498 | isa = XCConfigurationList; 499 | buildConfigurations = ( 500 | A74F6EE3216D5CB50054AA18 /* Debug */, 501 | A74F6EE4216D5CB50054AA18 /* Release */, 502 | ); 503 | defaultConfigurationIsVisible = 0; 504 | defaultConfigurationName = Release; 505 | }; 506 | A74F6EE5216D5CB50054AA18 /* Build configuration list for PBXNativeTarget "AloeStackView" */ = { 507 | isa = XCConfigurationList; 508 | buildConfigurations = ( 509 | A74F6EE6216D5CB50054AA18 /* Debug */, 510 | A74F6EE7216D5CB50054AA18 /* Release */, 511 | ); 512 | defaultConfigurationIsVisible = 0; 513 | defaultConfigurationName = Release; 514 | }; 515 | A74F6EE8216D5CB50054AA18 /* Build configuration list for PBXNativeTarget "AloeStackViewTests" */ = { 516 | isa = XCConfigurationList; 517 | buildConfigurations = ( 518 | A74F6EE9216D5CB50054AA18 /* Debug */, 519 | A74F6EEA216D5CB50054AA18 /* Release */, 520 | ); 521 | defaultConfigurationIsVisible = 0; 522 | defaultConfigurationName = Release; 523 | }; 524 | /* End XCConfigurationList section */ 525 | }; 526 | rootObject = A74F6EC8216D5CB40054AA18 /* Project object */; 527 | } 528 | -------------------------------------------------------------------------------- /AloeStackView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AloeStackView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AloeStackView.xcodeproj/xcshareddata/xcschemes/AloeStackView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | ### One issue or bug per Pull Request 2 | 3 | Keep your Pull Requests small. Small PRs are easier to reason about which makes them significantly more likely to get merged. 4 | 5 | ### Issues before features 6 | 7 | If you want to add a feature, consider filing an [Issue](../../issues). An Issue can provide the opportunity to discuss the requirements and implications of a feature with you before you start writing code. This is not a hard requirement, however. Submitting a Pull Request to demonstrate an idea in code is also acceptable, it just carries more risk of change. 8 | 9 | ### Backwards compatibility 10 | 11 | Respect the minimum deployment target. If you are adding code that uses new APIs, make sure to prevent older clients from crashing or misbehaving. Our CI runs against our minimum deployment targets, so you will not get a green build unless your code is backwards compatible. 12 | 13 | ### Forwards compatibility 14 | 15 | Please do not write new code using deprecated APIs. 16 | -------------------------------------------------------------------------------- /Docs/Images/add_many_rows.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/add_many_rows.gif -------------------------------------------------------------------------------- /Docs/Images/add_rows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/add_rows.png -------------------------------------------------------------------------------- /Docs/Images/dynamically_adjust_content.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/dynamically_adjust_content.gif -------------------------------------------------------------------------------- /Docs/Images/example_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/example_app.gif -------------------------------------------------------------------------------- /Docs/Images/hide_last_separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/hide_last_separator.png -------------------------------------------------------------------------------- /Docs/Images/hide_separators_by_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/hide_separators_by_default.png -------------------------------------------------------------------------------- /Docs/Images/large_blue_separators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/large_blue_separators.png -------------------------------------------------------------------------------- /Docs/Images/tap_handler.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/tap_handler.gif -------------------------------------------------------------------------------- /Docs/Images/tappable_protocol.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/tappable_protocol.gif -------------------------------------------------------------------------------- /Docs/Images/zero_separator_inset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Docs/Images/zero_separator_inset.png -------------------------------------------------------------------------------- /Example/AloeStackViewExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4D42E5BA22E499FE00D51F93 /* AloeStackView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7497A9021746D9700BB692A /* AloeStackView.framework */; }; 11 | 4D42E5BB22E499FE00D51F93 /* AloeStackView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A7497A9021746D9700BB692A /* AloeStackView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | A7497A932174725700BB692A /* SwitchRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7497A922174725700BB692A /* SwitchRowView.swift */; }; 13 | A7497A9721747DD600BB692A /* PhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7497A9621747DD600BB692A /* PhotoViewController.swift */; }; 14 | A7497A9921755AD100BB692A /* ExpandingRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7497A9821755AD100BB692A /* ExpandingRowView.swift */; }; 15 | A74F70B9217155FC0054AA18 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F70B8217155FC0054AA18 /* AppDelegate.swift */; }; 16 | A74F70BB217155FC0054AA18 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74F70BA217155FC0054AA18 /* MainViewController.swift */; }; 17 | A74F70C0217155FE0054AA18 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A74F70BF217155FE0054AA18 /* Assets.xcassets */; }; 18 | A74F70C3217155FE0054AA18 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A74F70C1217155FE0054AA18 /* LaunchScreen.storyboard */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXCopyFilesBuildPhase section */ 22 | 4D42E5BC22E499FE00D51F93 /* Embed Frameworks */ = { 23 | isa = PBXCopyFilesBuildPhase; 24 | buildActionMask = 2147483647; 25 | dstPath = ""; 26 | dstSubfolderSpec = 10; 27 | files = ( 28 | 4D42E5BB22E499FE00D51F93 /* AloeStackView.framework in Embed Frameworks */, 29 | ); 30 | name = "Embed Frameworks"; 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXCopyFilesBuildPhase section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | A7420FAB21715DF500F2A343 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | A7497A9021746D9700BB692A /* AloeStackView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AloeStackView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | A7497A922174725700BB692A /* SwitchRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchRowView.swift; sourceTree = ""; }; 39 | A7497A9621747DD600BB692A /* PhotoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoViewController.swift; sourceTree = ""; }; 40 | A7497A9821755AD100BB692A /* ExpandingRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingRowView.swift; sourceTree = ""; }; 41 | A74F70B5217155FC0054AA18 /* AloeStackViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AloeStackViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | A74F70B8217155FC0054AA18 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | A74F70BA217155FC0054AA18 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 44 | A74F70BF217155FE0054AA18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 45 | A74F70C2217155FE0054AA18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFrameworksBuildPhase section */ 49 | A74F70B2217155FB0054AA18 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | 4D42E5BA22E499FE00D51F93 /* AloeStackView.framework in Frameworks */, 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | A7420FA521715B0000F2A343 /* ViewControllers */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | A74F70BA217155FC0054AA18 /* MainViewController.swift */, 64 | A7497A9621747DD600BB692A /* PhotoViewController.swift */, 65 | ); 66 | path = ViewControllers; 67 | sourceTree = ""; 68 | }; 69 | A7420FA621715B4300F2A343 /* App */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | A74F70B8217155FC0054AA18 /* AppDelegate.swift */, 73 | ); 74 | path = App; 75 | sourceTree = ""; 76 | }; 77 | A7420FA721715C9300F2A343 /* Assets */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | A74F70BF217155FE0054AA18 /* Assets.xcassets */, 81 | A74F70C1217155FE0054AA18 /* LaunchScreen.storyboard */, 82 | ); 83 | path = Assets; 84 | sourceTree = ""; 85 | }; 86 | A7420FA821715CAC00F2A343 /* Config */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | A7420FAB21715DF500F2A343 /* Info.plist */, 90 | ); 91 | path = Config; 92 | sourceTree = ""; 93 | }; 94 | A7420FA921715CC400F2A343 /* Views */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | A7497A922174725700BB692A /* SwitchRowView.swift */, 98 | A7497A9821755AD100BB692A /* ExpandingRowView.swift */, 99 | ); 100 | path = Views; 101 | sourceTree = ""; 102 | }; 103 | A7420FAA21715CCF00F2A343 /* Other */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | A7420FA621715B4300F2A343 /* App */, 107 | A7420FA821715CAC00F2A343 /* Config */, 108 | ); 109 | path = Other; 110 | sourceTree = ""; 111 | }; 112 | A7497A8F21746D9700BB692A /* Frameworks */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | A7497A9021746D9700BB692A /* AloeStackView.framework */, 116 | ); 117 | name = Frameworks; 118 | sourceTree = ""; 119 | }; 120 | A74F70AC217155FB0054AA18 = { 121 | isa = PBXGroup; 122 | children = ( 123 | A74F70B7217155FC0054AA18 /* AloeStackViewExample */, 124 | A74F70B6217155FC0054AA18 /* Products */, 125 | A7497A8F21746D9700BB692A /* Frameworks */, 126 | ); 127 | sourceTree = ""; 128 | }; 129 | A74F70B6217155FC0054AA18 /* Products */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | A74F70B5217155FC0054AA18 /* AloeStackViewExample.app */, 133 | ); 134 | name = Products; 135 | sourceTree = ""; 136 | }; 137 | A74F70B7217155FC0054AA18 /* AloeStackViewExample */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | A7420FA721715C9300F2A343 /* Assets */, 141 | A7420FA921715CC400F2A343 /* Views */, 142 | A7420FA521715B0000F2A343 /* ViewControllers */, 143 | A7420FAA21715CCF00F2A343 /* Other */, 144 | ); 145 | path = AloeStackViewExample; 146 | sourceTree = ""; 147 | }; 148 | /* End PBXGroup section */ 149 | 150 | /* Begin PBXNativeTarget section */ 151 | A74F70B4217155FB0054AA18 /* AloeStackViewExample */ = { 152 | isa = PBXNativeTarget; 153 | buildConfigurationList = A74F70C7217155FE0054AA18 /* Build configuration list for PBXNativeTarget "AloeStackViewExample" */; 154 | buildPhases = ( 155 | A74F70B1217155FB0054AA18 /* Sources */, 156 | A74F70B2217155FB0054AA18 /* Frameworks */, 157 | A74F70B3217155FB0054AA18 /* Resources */, 158 | 4D42E5BC22E499FE00D51F93 /* Embed Frameworks */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = AloeStackViewExample; 165 | productName = AloeStackViewExample; 166 | productReference = A74F70B5217155FC0054AA18 /* AloeStackViewExample.app */; 167 | productType = "com.apple.product-type.application"; 168 | }; 169 | /* End PBXNativeTarget section */ 170 | 171 | /* Begin PBXProject section */ 172 | A74F70AD217155FB0054AA18 /* Project object */ = { 173 | isa = PBXProject; 174 | attributes = { 175 | LastSwiftUpdateCheck = 1000; 176 | LastUpgradeCheck = 1000; 177 | ORGANIZATIONNAME = ""; 178 | TargetAttributes = { 179 | A74F70B4217155FB0054AA18 = { 180 | CreatedOnToolsVersion = 10.0; 181 | }; 182 | }; 183 | }; 184 | buildConfigurationList = A74F70B0217155FB0054AA18 /* Build configuration list for PBXProject "AloeStackViewExample" */; 185 | compatibilityVersion = "Xcode 9.3"; 186 | developmentRegion = en; 187 | hasScannedForEncodings = 0; 188 | knownRegions = ( 189 | en, 190 | Base, 191 | ); 192 | mainGroup = A74F70AC217155FB0054AA18; 193 | productRefGroup = A74F70B6217155FC0054AA18 /* Products */; 194 | projectDirPath = ""; 195 | projectRoot = ""; 196 | targets = ( 197 | A74F70B4217155FB0054AA18 /* AloeStackViewExample */, 198 | ); 199 | }; 200 | /* End PBXProject section */ 201 | 202 | /* Begin PBXResourcesBuildPhase section */ 203 | A74F70B3217155FB0054AA18 /* Resources */ = { 204 | isa = PBXResourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | A74F70C3217155FE0054AA18 /* LaunchScreen.storyboard in Resources */, 208 | A74F70C0217155FE0054AA18 /* Assets.xcassets in Resources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXResourcesBuildPhase section */ 213 | 214 | /* Begin PBXSourcesBuildPhase section */ 215 | A74F70B1217155FB0054AA18 /* Sources */ = { 216 | isa = PBXSourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | A74F70BB217155FC0054AA18 /* MainViewController.swift in Sources */, 220 | A7497A9721747DD600BB692A /* PhotoViewController.swift in Sources */, 221 | A74F70B9217155FC0054AA18 /* AppDelegate.swift in Sources */, 222 | A7497A9921755AD100BB692A /* ExpandingRowView.swift in Sources */, 223 | A7497A932174725700BB692A /* SwitchRowView.swift in Sources */, 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | /* End PBXSourcesBuildPhase section */ 228 | 229 | /* Begin PBXVariantGroup section */ 230 | A74F70C1217155FE0054AA18 /* LaunchScreen.storyboard */ = { 231 | isa = PBXVariantGroup; 232 | children = ( 233 | A74F70C2217155FE0054AA18 /* Base */, 234 | ); 235 | name = LaunchScreen.storyboard; 236 | sourceTree = ""; 237 | }; 238 | /* End PBXVariantGroup section */ 239 | 240 | /* Begin XCBuildConfiguration section */ 241 | A74F70C5217155FE0054AA18 /* Debug */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | ALWAYS_SEARCH_USER_PATHS = NO; 245 | CLANG_ANALYZER_NONNULL = YES; 246 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 248 | CLANG_CXX_LIBRARY = "libc++"; 249 | CLANG_ENABLE_MODULES = YES; 250 | CLANG_ENABLE_OBJC_ARC = YES; 251 | CLANG_ENABLE_OBJC_WEAK = YES; 252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 253 | CLANG_WARN_BOOL_CONVERSION = YES; 254 | CLANG_WARN_COMMA = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 259 | CLANG_WARN_EMPTY_BODY = YES; 260 | CLANG_WARN_ENUM_CONVERSION = YES; 261 | CLANG_WARN_INFINITE_RECURSION = YES; 262 | CLANG_WARN_INT_CONVERSION = YES; 263 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 265 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 267 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 268 | CLANG_WARN_STRICT_PROTOTYPES = YES; 269 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 270 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 271 | CLANG_WARN_UNREACHABLE_CODE = YES; 272 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 273 | CODE_SIGN_IDENTITY = "iPhone Developer"; 274 | COPY_PHASE_STRIP = NO; 275 | DEBUG_INFORMATION_FORMAT = dwarf; 276 | ENABLE_STRICT_OBJC_MSGSEND = YES; 277 | ENABLE_TESTABILITY = YES; 278 | GCC_C_LANGUAGE_STANDARD = gnu11; 279 | GCC_DYNAMIC_NO_PIC = NO; 280 | GCC_NO_COMMON_BLOCKS = YES; 281 | GCC_OPTIMIZATION_LEVEL = 0; 282 | GCC_PREPROCESSOR_DEFINITIONS = ( 283 | "DEBUG=1", 284 | "$(inherited)", 285 | ); 286 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 287 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 288 | GCC_WARN_UNDECLARED_SELECTOR = YES; 289 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 290 | GCC_WARN_UNUSED_FUNCTION = YES; 291 | GCC_WARN_UNUSED_VARIABLE = YES; 292 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 293 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 294 | MTL_FAST_MATH = YES; 295 | ONLY_ACTIVE_ARCH = YES; 296 | SDKROOT = iphoneos; 297 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 298 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 299 | }; 300 | name = Debug; 301 | }; 302 | A74F70C6217155FE0054AA18 /* Release */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 309 | CLANG_CXX_LIBRARY = "libc++"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_ENABLE_OBJC_WEAK = YES; 313 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 314 | CLANG_WARN_BOOL_CONVERSION = YES; 315 | CLANG_WARN_COMMA = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 319 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INFINITE_RECURSION = YES; 323 | CLANG_WARN_INT_CONVERSION = YES; 324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 329 | CLANG_WARN_STRICT_PROTOTYPES = YES; 330 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 331 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 332 | CLANG_WARN_UNREACHABLE_CODE = YES; 333 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 334 | CODE_SIGN_IDENTITY = "iPhone Developer"; 335 | COPY_PHASE_STRIP = NO; 336 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 337 | ENABLE_NS_ASSERTIONS = NO; 338 | ENABLE_STRICT_OBJC_MSGSEND = YES; 339 | GCC_C_LANGUAGE_STANDARD = gnu11; 340 | GCC_NO_COMMON_BLOCKS = YES; 341 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 342 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 343 | GCC_WARN_UNDECLARED_SELECTOR = YES; 344 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 345 | GCC_WARN_UNUSED_FUNCTION = YES; 346 | GCC_WARN_UNUSED_VARIABLE = YES; 347 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 348 | MTL_ENABLE_DEBUG_INFO = NO; 349 | MTL_FAST_MATH = YES; 350 | SDKROOT = iphoneos; 351 | SWIFT_COMPILATION_MODE = wholemodule; 352 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 353 | VALIDATE_PRODUCT = YES; 354 | }; 355 | name = Release; 356 | }; 357 | A74F70C8217155FE0054AA18 /* Debug */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 361 | CODE_SIGN_STYLE = Automatic; 362 | DEVELOPMENT_TEAM = 5LL7P8E8RA; 363 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 364 | INFOPLIST_FILE = AloeStackViewExample/Other/Config/Info.plist; 365 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 366 | LD_RUNPATH_SEARCH_PATHS = ( 367 | "$(inherited)", 368 | "@executable_path/Frameworks", 369 | ); 370 | PRODUCT_BUNDLE_IDENTIFIER = com.oshlack.marli.AloeStackViewExample; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | SWIFT_VERSION = 5.0; 373 | TARGETED_DEVICE_FAMILY = "1,2"; 374 | }; 375 | name = Debug; 376 | }; 377 | A74F70C9217155FE0054AA18 /* Release */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 381 | CODE_SIGN_STYLE = Automatic; 382 | DEVELOPMENT_TEAM = 5LL7P8E8RA; 383 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 384 | INFOPLIST_FILE = AloeStackViewExample/Other/Config/Info.plist; 385 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 386 | LD_RUNPATH_SEARCH_PATHS = ( 387 | "$(inherited)", 388 | "@executable_path/Frameworks", 389 | ); 390 | PRODUCT_BUNDLE_IDENTIFIER = com.oshlack.marli.AloeStackViewExample; 391 | PRODUCT_NAME = "$(TARGET_NAME)"; 392 | SWIFT_VERSION = 5.0; 393 | TARGETED_DEVICE_FAMILY = "1,2"; 394 | }; 395 | name = Release; 396 | }; 397 | /* End XCBuildConfiguration section */ 398 | 399 | /* Begin XCConfigurationList section */ 400 | A74F70B0217155FB0054AA18 /* Build configuration list for PBXProject "AloeStackViewExample" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | A74F70C5217155FE0054AA18 /* Debug */, 404 | A74F70C6217155FE0054AA18 /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | A74F70C7217155FE0054AA18 /* Build configuration list for PBXNativeTarget "AloeStackViewExample" */ = { 410 | isa = XCConfigurationList; 411 | buildConfigurations = ( 412 | A74F70C8217155FE0054AA18 /* Debug */, 413 | A74F70C9217155FE0054AA18 /* Release */, 414 | ); 415 | defaultConfigurationIsVisible = 0; 416 | defaultConfigurationName = Release; 417 | }; 418 | /* End XCConfigurationList section */ 419 | }; 420 | rootObject = A74F70AD217155FB0054AA18 /* Project object */; 421 | } 422 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Assets/Assets.xcassets/lobster-dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "lobster-dog.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Assets/Assets.xcassets/lobster-dog.imageset/lobster-dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marlimox/AloeStackView/001e71afa4a74185d0811b76ca857ebebd387094/Example/AloeStackViewExample/Assets/Assets.xcassets/lobster-dog.imageset/lobster-dog.jpg -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Assets/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Other/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 10/12/18. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import UIKit 17 | 18 | @UIApplicationMain 19 | class AppDelegate: UIResponder, UIApplicationDelegate { 20 | 21 | var window: UIWindow? 22 | 23 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 24 | window = UIWindow(frame: UIScreen.main.bounds) 25 | let homeViewController = MainViewController() 26 | let navigationController = UINavigationController(rootViewController: homeViewController) 27 | window?.rootViewController = navigationController 28 | window?.makeKeyAndVisible() 29 | return true 30 | } 31 | 32 | func applicationWillResignActive(_ application: UIApplication) { 33 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 34 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 35 | } 36 | 37 | func applicationDidEnterBackground(_ application: UIApplication) { 38 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 39 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 40 | } 41 | 42 | func applicationWillEnterForeground(_ application: UIApplication) { 43 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 44 | } 45 | 46 | func applicationDidBecomeActive(_ application: UIApplication) { 47 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 48 | } 49 | 50 | func applicationWillTerminate(_ application: UIApplication) { 51 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 52 | } 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Other/Config/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample/ViewControllers/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 10/12/18. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import AloeStackView 17 | import UIKit 18 | 19 | public class MainViewController: AloeStackViewController { 20 | 21 | // MARK: Public 22 | 23 | public override func viewDidLoad() { 24 | super.viewDidLoad() 25 | setUpSelf() 26 | setUpStackView() 27 | setUpRows() 28 | } 29 | 30 | // MARK: Private 31 | 32 | private func setUpSelf() { 33 | title = "AloeStackView Example" 34 | } 35 | 36 | private func setUpStackView() { 37 | stackView.automaticallyHidesLastSeparator = true 38 | } 39 | 40 | private func setUpRows() { 41 | setUpDescriptionRow() 42 | setUpSwitchRow() 43 | setUpHiddenRows() 44 | setUpExpandingRowView() 45 | setUpHorizontalRow() 46 | setUpPhotoRow() 47 | } 48 | 49 | private func setUpDescriptionRow() { 50 | let label = UILabel() 51 | label.font = UIFont.preferredFont(forTextStyle: .body) 52 | label.numberOfLines = 0 53 | label.text = "This simple app shows some ways you can use AloeStackView to lay out a screen in your app." 54 | stackView.addRow(label) 55 | } 56 | 57 | private func setUpSwitchRow() { 58 | let switchRow = SwitchRowView() 59 | switchRow.text = "Show and hide rows with animation" 60 | switchRow.switchDidChange = { [weak self] isOn in 61 | guard let self = self else { return } 62 | self.stackView.setRowsHidden(self.hiddenRows, isHidden: !isOn, animated: true) 63 | } 64 | stackView.addRow(switchRow) 65 | } 66 | 67 | private let hiddenRows = [UILabel(), UILabel(), UILabel(), UILabel(), UILabel()] 68 | 69 | private func setUpHiddenRows() { 70 | for (index, row) in hiddenRows.enumerated() { 71 | row.font = UIFont.preferredFont(forTextStyle: .caption2) 72 | row.text = "Hidden row " + String(index + 1) 73 | } 74 | 75 | stackView.addRows(hiddenRows) 76 | stackView.hideRows(hiddenRows) 77 | 78 | let rowInset = UIEdgeInsets( 79 | top: stackView.rowInset.top, 80 | left: stackView.rowInset.left * 2, 81 | bottom: stackView.rowInset.bottom, 82 | right: stackView.rowInset.right) 83 | 84 | let separatorInset = UIEdgeInsets( 85 | top: 0, 86 | left: stackView.separatorInset.left * 2, 87 | bottom: 0, 88 | right: 0) 89 | 90 | stackView.setInset(forRows: hiddenRows, inset: rowInset) 91 | stackView.setSeparatorInset(forRows: Array(hiddenRows.dropLast()), inset: separatorInset) 92 | } 93 | 94 | private func setUpExpandingRowView() { 95 | let expandingRow = ExpandingRowView() 96 | stackView.addRow(expandingRow) 97 | } 98 | 99 | private func setUpHorizontalRow() { 100 | let titleLabel = UILabel() 101 | titleLabel.font = UIFont.preferredFont(forTextStyle: .body) 102 | titleLabel.numberOfLines = 0 103 | titleLabel.text = "Use a horizontal layout" 104 | stackView.addRow(titleLabel) 105 | stackView.hideSeparator(forRow: titleLabel) 106 | stackView.setInset(forRow: titleLabel, inset: UIEdgeInsets( 107 | top: stackView.rowInset.top, 108 | left: stackView.rowInset.left, 109 | bottom: 4, 110 | right: stackView.rowInset.right)) 111 | 112 | let captionLabel = UILabel() 113 | captionLabel.font = UIFont.preferredFont(forTextStyle: .caption2) 114 | captionLabel.textColor = .blue 115 | captionLabel.numberOfLines = 0 116 | captionLabel.text = "(Try scrolling horizontally!)" 117 | stackView.addRow(captionLabel) 118 | stackView.hideSeparator(forRow: captionLabel) 119 | stackView.setInset(forRow: captionLabel, inset: UIEdgeInsets( 120 | top: 0, 121 | left: stackView.rowInset.left, 122 | bottom: stackView.rowInset.bottom, 123 | right: stackView.rowInset.right)) 124 | 125 | let horizontalStackView = AloeStackView() 126 | horizontalStackView.axis = .horizontal 127 | horizontalStackView.hidesSeparatorsByDefault = true 128 | horizontalStackView.showsHorizontalScrollIndicator = false 129 | 130 | horizontalStackView.contentInset = UIEdgeInsets( 131 | top: 0, 132 | left: stackView.rowInset.left / 2, 133 | bottom: 0, 134 | right: stackView.rowInset.right / 2) 135 | 136 | horizontalStackView.rowInset = UIEdgeInsets( 137 | top: stackView.rowInset.top, 138 | left: stackView.rowInset.left / 2, 139 | bottom: stackView.rowInset.bottom, 140 | right: stackView.rowInset.right / 2) 141 | 142 | horizontalStackView.heightAnchor.constraint(equalToConstant: 120).isActive = true 143 | 144 | stackView.addRow(horizontalStackView) 145 | stackView.setInset(forRow: horizontalStackView, inset: .zero) 146 | 147 | guard let image = UIImage(named: "lobster-dog") else { return } 148 | 149 | for imageNumber in 1...10 { 150 | let imageView = UIImageView(image: image) 151 | imageView.isUserInteractionEnabled = true 152 | imageView.contentMode = .scaleAspectFit 153 | 154 | let aspectRatio = image.size.height / image.size.width 155 | imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: aspectRatio).isActive = true 156 | 157 | horizontalStackView.addRow(imageView) 158 | 159 | horizontalStackView.setTapHandler(forRow: imageView) { [weak self] _ in 160 | let alert = UIAlertController( 161 | title: "Tapped on image \(imageNumber)", 162 | message: nil, 163 | preferredStyle: .alert) 164 | alert.addAction(UIAlertAction(title: "Dismiss", style: .default)) 165 | self?.present(alert, animated: true) 166 | } 167 | } 168 | } 169 | 170 | private func setUpPhotoRow() { 171 | let titleLabel = UILabel() 172 | titleLabel.font = UIFont.preferredFont(forTextStyle: .body) 173 | titleLabel.numberOfLines = 0 174 | titleLabel.text = "Handle user interaction" 175 | stackView.addRow(titleLabel) 176 | stackView.hideSeparator(forRow: titleLabel) 177 | stackView.setInset(forRow: titleLabel, inset: UIEdgeInsets( 178 | top: stackView.rowInset.top, 179 | left: stackView.rowInset.left, 180 | bottom: 4, 181 | right: stackView.rowInset.right)) 182 | 183 | let captionLabel = UILabel() 184 | captionLabel.font = UIFont.preferredFont(forTextStyle: .caption2) 185 | captionLabel.textColor = .blue 186 | captionLabel.numberOfLines = 0 187 | captionLabel.text = "(Try tapping on the photo!)" 188 | stackView.addRow(captionLabel) 189 | stackView.hideSeparator(forRow: captionLabel) 190 | stackView.setInset(forRow: captionLabel, inset: UIEdgeInsets( 191 | top: 0, 192 | left: stackView.rowInset.left, 193 | bottom: stackView.rowInset.bottom, 194 | right: stackView.rowInset.right)) 195 | 196 | guard let image = UIImage(named: "lobster-dog") else { return } 197 | let aspectRatio = image.size.height / image.size.width 198 | 199 | let imageView = UIImageView(image: image) 200 | imageView.isUserInteractionEnabled = true 201 | imageView.contentMode = .scaleAspectFit 202 | imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: aspectRatio).isActive = true 203 | 204 | stackView.addRow(imageView) 205 | stackView.setTapHandler(forRow: imageView) { [weak self] _ in 206 | guard let self = self else { return } 207 | let vc = PhotoViewController() 208 | self.navigationController?.pushViewController(vc, animated: true) 209 | } 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample/ViewControllers/PhotoViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 10/12/18. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import AloeStackView 17 | import UIKit 18 | 19 | public class PhotoViewController: AloeStackViewController { 20 | 21 | // MARK: Public 22 | 23 | public override func viewDidLoad() { 24 | super.viewDidLoad() 25 | setUpSelf() 26 | setUpStackView() 27 | setUpRows() 28 | } 29 | 30 | // MARK: Private 31 | 32 | private func setUpSelf() { 33 | title = "Photo" 34 | } 35 | 36 | private func setUpStackView() { 37 | stackView.hidesSeparatorsByDefault = true 38 | } 39 | 40 | private func setUpRows() { 41 | setUpImageRow() 42 | } 43 | 44 | private func setUpImageRow() { 45 | guard let image = UIImage(named: "lobster-dog") else { return } 46 | let aspectRatio = image.size.height / image.size.width 47 | 48 | let imageView = UIImageView(image: image) 49 | imageView.contentMode = .scaleAspectFit 50 | imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: aspectRatio).isActive = true 51 | 52 | stackView.addRow(imageView) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Views/ExpandingRowView.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 10/14/18. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import AloeStackView 17 | import UIKit 18 | 19 | public class ExpandingRowView: UIStackView, Tappable, Highlightable { 20 | 21 | // MARK: Lifecycle 22 | 23 | public init() { 24 | super.init(frame: .zero) 25 | translatesAutoresizingMaskIntoConstraints = false 26 | setUpViews() 27 | } 28 | 29 | public required init(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | // MARK: Public 34 | 35 | public func didTapView() { 36 | textLabel.text = (textLabel.text ?? "") + "\n" + lines[nextLine] 37 | nextLine += 1 38 | if nextLine == lines.count { 39 | nextLine = 0 40 | } 41 | } 42 | 43 | // MARK: Private 44 | 45 | private let titleLabel = UILabel() 46 | private let showMoreLabel = UILabel() 47 | private let textLabel = UILabel() 48 | 49 | private var nextLine = 1 50 | 51 | private func setUpViews() { 52 | setUpSelf() 53 | setUpTitleLabel() 54 | setUpShowMoreLabel() 55 | setUpTextLabel() 56 | } 57 | 58 | private func setUpSelf() { 59 | axis = .vertical 60 | spacing = 4 61 | } 62 | 63 | private func setUpTitleLabel() { 64 | titleLabel.text = "Dynamically change row content" 65 | titleLabel.font = UIFont.preferredFont(forTextStyle: .body) 66 | addArrangedSubview(titleLabel) 67 | } 68 | 69 | private func setUpShowMoreLabel() { 70 | showMoreLabel.numberOfLines = 0 71 | showMoreLabel.text = "(Tap on this row to add more content!)\n" 72 | showMoreLabel.font = UIFont.preferredFont(forTextStyle: .caption2) 73 | showMoreLabel.textColor = .blue 74 | addArrangedSubview(showMoreLabel) 75 | } 76 | 77 | private func setUpTextLabel() { 78 | textLabel.numberOfLines = 0 79 | textLabel.font = UIFont.preferredFont(forTextStyle: .caption2) 80 | textLabel.text = lines[0] 81 | addArrangedSubview(textLabel) 82 | } 83 | 84 | private let lines = [ 85 | "Two households, both alike in dignity,", 86 | "In fair Verona, where we lay our scene,", 87 | "From ancient grudge break to new mutiny,", 88 | "Where civil blood makes civil hands unclean.", 89 | "From forth the fatal loins of these two foes", 90 | "A pair of star-cross'd lovers take their life;", 91 | "Whose misadventured piteous overthrows", 92 | "Do with their death bury their parents' strife.", 93 | "The fearful passage of their death-mark'd love,", 94 | "And the continuance of their parents' rage,", 95 | "Which, but their children's end, nought could remove,", 96 | "Is now the two hours' traffic of our stage;", 97 | "The which if you with patient ears attend,", 98 | "What here shall miss, our toil shall strive to mend." 99 | ] 100 | 101 | } 102 | -------------------------------------------------------------------------------- /Example/AloeStackViewExample/Views/SwitchRowView.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 10/14/18. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import AloeStackView 17 | import UIKit 18 | 19 | public class SwitchRowView: UIView { 20 | 21 | // MARK: Lifecycle 22 | 23 | public init() { 24 | super.init(frame: .zero) 25 | setUpViews() 26 | setUpConstraints() 27 | } 28 | 29 | public required init(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | // MARK: Public 34 | 35 | public var text: String? { 36 | get { return label.text } 37 | set { label.text = newValue} 38 | } 39 | 40 | public var switchDidChange: ((_ isOn: Bool) -> Void)? 41 | 42 | // MARK: Private 43 | 44 | private let label = UILabel() 45 | private let switchView = UISwitch(frame: .zero) 46 | 47 | private func setUpViews() { 48 | setUpLabel() 49 | setUpSwitchView() 50 | } 51 | 52 | private func setUpLabel() { 53 | label.translatesAutoresizingMaskIntoConstraints = false 54 | label.font = UIFont.preferredFont(forTextStyle: .body) 55 | addSubview(label) 56 | } 57 | 58 | private func setUpSwitchView() { 59 | switchView.translatesAutoresizingMaskIntoConstraints = false 60 | switchView.addTarget(self, action: #selector(switchChanged), for: .valueChanged) 61 | addSubview(switchView) 62 | } 63 | 64 | @objc private func switchChanged() { 65 | switchDidChange?(switchView.isOn) 66 | } 67 | 68 | private func setUpConstraints() { 69 | NSLayoutConstraint.activate([ 70 | label.topAnchor.constraint(equalTo: topAnchor), 71 | label.bottomAnchor.constraint(equalTo: bottomAnchor), 72 | label.leadingAnchor.constraint(equalTo: leadingAnchor), 73 | switchView.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8), 74 | switchView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -2), 75 | switchView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -1) 76 | ]) 77 | } 78 | 79 | } 80 | 81 | extension SwitchRowView: Tappable { 82 | 83 | public func didTapView() { 84 | switchView.setOn(!switchView.isOn, animated: true) 85 | switchView.sendActions(for: .valueChanged) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' do 2 | gem 'cocoapods', '~> 1.6.0' 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | cocoapods (1.6.1) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.6.1) 16 | cocoapods-deintegrate (>= 1.0.2, < 2.0) 17 | cocoapods-downloader (>= 1.2.2, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.1, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (>= 2.2.0, < 3.0) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.6) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.4) 30 | xcodeproj (>= 1.8.1, < 2.0) 31 | cocoapods-core (1.6.1) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.4) 36 | cocoapods-downloader (1.2.2) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.1.0) 41 | cocoapods-trunk (1.3.1) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.1.5) 47 | escape (0.0.4) 48 | fourflusher (2.2.0) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.6) 55 | nanaimo (0.2.6) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | ruby-macho (1.4.0) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.5) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.8.2) 63 | CFPropertyList (>= 2.3.3, < 4.0) 64 | atomos (~> 0.1.3) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.6) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | cocoapods (~> 1.6.0)! 74 | 75 | BUNDLED WITH 76 | 2.0.1 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright Marli Oshlack 2018. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AloeStackView", 7 | platforms: [ 8 | .iOS(.v9), 9 | ], 10 | products: [ 11 | .library( 12 | name: "AloeStackView", 13 | targets: ["AloeStackView"]), 14 | ], 15 | dependencies: [], 16 | targets: [ 17 | .target( 18 | name: "AloeStackView", 19 | dependencies: [], 20 | path: "Sources"), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AloeStackView 2 | 3 | A simple class for laying out a collection of views with a convenient API, while leveraging the power of Auto Layout. 4 | 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![Version](https://img.shields.io/cocoapods/v/AloeStackView.svg)](https://cocoapods.org/pods/AloeStackView) 7 | [![License](https://img.shields.io/cocoapods/l/AloeStackView.svg)](https://cocoapods.org/pods/AloeStackView) 8 | [![Platform](https://img.shields.io/cocoapods/p/AloeStackView.svg)](https://cocoapods.org/pods/AloeStackView) 9 | 10 | ## Introduction 11 | 12 | `AloeStackView` is a class that allows a collection of views to be laid out in a vertical or horizontal list. In a broad 13 | sense, it is similar to `UITableView`, however its implementation is quite different and it makes a different set of 14 | trade-offs. 15 | 16 | `AloeStackView` focuses first and foremost on making UI very quick, simple, and straightforward to implement. It 17 | does this in two ways: 18 | 19 | * It leverages the power of Auto Layout to automatically update the UI when making changes to views. 20 | 21 | * It forgoes some features of `UITableView`, such as view recycling, in order to achieve a much simpler and safer API. 22 | 23 | We've found `AloeStackView` to be a useful piece of infrastructure and hope you find it useful too! 24 | 25 | ## Table of Contents 26 | 27 | * [Features](#features) 28 | * [System Requirements](#system-requirements) 29 | * [Example App](#example-app) 30 | * [Usage](#usage) 31 | - [Creating an AloeStackView](#creating-an-aloestackview) 32 | - [Adding, Removing, and Managing Rows](#adding-removing-and-managing-rows) 33 | - [Handling User Interaction](#handling-user-interaction) 34 | - [Dynamically Changing Row Content](#dynamically-changing-row-content) 35 | - [Styling and Controlling Separators](#styling-and-controlling-separators) 36 | - [Extending AloeStackView](#extending-aloestackview) 37 | - [When to use AloeStackView](#when-to-use-aloestackview) 38 | * [Installation](#installation) 39 | * [Contributions](#contributions) 40 | * [Maintainers](#maintainers) 41 | * [Contributors](#contributors) 42 | * [License](#license) 43 | * [Why is it called AloeStackView?](#why-is-it-called-aloestackview) 44 | 45 | ## Features 46 | 47 | * Allows you to keep strong references to views and dynamically change their properties, while Auto Layout 48 | automatically keeps the UI up-to-date. 49 | 50 | * Allows views to be dynamically added, removed, hidden and shown, with optional animation. 51 | 52 | * Includes built-in support for customizable separators between views. 53 | 54 | * Provides an extensible API, allowing specialized features to be added without modifying `AloeStackView` itself. 55 | 56 | * Widely used and vetted in a highly-trafficked iOS app. 57 | 58 | * Small, easy-to-understand codebase (under 600 lines of code) with no external dependencies keeps binary size 59 | increase to a minimum and makes code contributions and debugging painless. 60 | 61 | ## System Requirements 62 | 63 | * Deployment target iOS 9.0+ 64 | * Xcode 10.0+ 65 | * Swift 4.0+ 66 | 67 | ## Example App 68 | 69 | The repository includes a simple [example iOS app](Example). 70 | 71 | You can try it out by cloning the repo, opening `AloeStackViewExample.xcworkspace`, and running the app. 72 | 73 | The example app shows a few ways `AloeStackView` can be used to implement a screen in an iOS app. 74 | 75 | ![Example app](Docs/Images/example_app.gif) 76 | 77 | ## Usage 78 | 79 | ### Creating an AloeStackView 80 | 81 | The primary API is accessed via the `AloeStackView` class. 82 | 83 | You can create an instance of `AloeStackView` quite easily in your code: 84 | 85 | ```swift 86 | import AloeStackView 87 | 88 | let stackView = AloeStackView() 89 | ``` 90 | 91 | `AloeStackView` is a `UIView` (specifically a `UIScrollView`), and thus can be used in the same way as any other 92 | view in your app. 93 | 94 | Alternatively, if you want to build an entire `UIViewController` using `AloeStackView`, you can use the convenient 95 | `AloeStackViewController` class: 96 | 97 | ```swift 98 | import AloeStackView 99 | 100 | public class MyViewController: AloeStackViewController { 101 | 102 | public override func viewDidLoad() { 103 | super.viewDidLoad() 104 | stackView.addRow(...) 105 | } 106 | 107 | } 108 | ``` 109 | 110 | `AloeStackViewController` is very similar to classes such as `UITableViewController` and 111 | `UICollectionViewController` in that it creates and manages an `AloeStackView` for you. You can access the 112 | `AloeStackView` via the `stackView` property. Using `AloeStackViewController` rather than creating your own 113 | `AloeStackView` inside a `UIViewController` simply saves you some typing. 114 | 115 | ### Adding, Removing, and Managing Rows 116 | 117 | The API of `AloeStackView` generally deals with "rows". A row can be any `UIView` that you want to use in your UI. 118 | 119 | By default, rows are arranged in a vertical column, and each row stretches the full width of the `AloeStackView`. 120 | 121 | The `axis` property on `AloeStackView` can be used to change the orientation. When `axis` is set to `.horizontal`, 122 | rows are arranged next to each other, left-to-right, and the `AloeStackView` scrolls horizontally, with each row 123 | stretching the full height of the `AloeStackView`. 124 | 125 | To build a UI with `AloeStackView`, you generally begin by adding the rows that make up your UI: 126 | 127 | ```swift 128 | for i in 1...3 { 129 | let label = UILabel() 130 | label.text = "Label \(i)" 131 | stackView.addRow(label) 132 | } 133 | ``` 134 | ![Add rows](Docs/Images/add_rows.png) 135 | 136 | If the length of an `AloeStackView` ever grows too long for the available screen space, the content automatically 137 | becomes scrollable. 138 | 139 | ![Add rows](Docs/Images/add_many_rows.gif) 140 | 141 | `AloeStackView` provides a comprehensive set of methods for managing rows, including inserting rows at the 142 | beginning and end, inserting rows above or below other rows, hiding and showing rows, removing rows, and retrieving 143 | rows. 144 | 145 | You can customize the spacing around a row with the `rowInset` property, and the `setInset(forRow:)` and 146 | `setInset(forRows:)` methods. 147 | 148 | The class documentation in [AloeStackView.swift](Sources/AloeStackView/AloeStackView.swift) provides full details of 149 | all the APIs available. 150 | 151 | ### Handling User Interaction 152 | 153 | `AloeStackView` provides support for handling tap gestures on a row: 154 | 155 | ```swift 156 | stackView.setTapHandler( 157 | forRow: label, 158 | handler: { [weak self] label in 159 | self?.showAlert(title: "Row Tapped", message: "Tapped on: \(label.text ?? "")") 160 | }) 161 | 162 | label.isUserInteractionEnabled = true 163 | ``` 164 | ![Add rows](Docs/Images/tap_handler.gif) 165 | 166 | A tap handler will only fire if `isUserInteractionEnabled` is `true` for a row. 167 | 168 | Another way of handling tap gestures is to conform to the `Tappable` protocol: 169 | 170 | ```swift 171 | public class ToggleLabel: UILabel, Tappable { 172 | 173 | public func didTapView() { 174 | textColor = textColor == .red ? .black : .red 175 | } 176 | 177 | } 178 | 179 | for i in 1...3 { 180 | let label = ToggleLabel() 181 | label.text = "Label \(i)" 182 | label.isUserInteractionEnabled = true 183 | stackView.addRow(label) 184 | } 185 | ``` 186 | ![Add rows](Docs/Images/tappable_protocol.gif) 187 | 188 | Conforming to `Tappable` allows common tap gesture handling behavior to be encapsulated inside a view. This way 189 | you can reuse a view in an `AloeStackView` many times, without writing the same tap gesture handling code each 190 | time. 191 | 192 | ### Dynamically Changing Row Content 193 | 194 | One of the advantages of using `AloeStackView` is that you can keep a strong reference to a view even after you've 195 | added it to an `AloeStackView`. 196 | 197 | If you change a property of a view that affects the layout of the overall UI, `AloeStackView` will automatically relayout 198 | all of its rows: 199 | 200 | ```swift 201 | stackView.setTapHandler(forRow: label, handler: { label in 202 | label.text = (label.text ?? "") + "\n\nSome more text!" 203 | }) 204 | ``` 205 | ![Add rows](Docs/Images/dynamically_adjust_content.gif) 206 | 207 | As you can see, there's no need to notify `AloeStackView` before or after making changes to a view. Auto Layout will 208 | ensure that the UI remains in an up-to-date state. 209 | 210 | ### Styling and Controlling Separators 211 | 212 | `AloeStackView` adds separators between rows by default: 213 | 214 | ![Add rows](Docs/Images/add_rows.png) 215 | 216 | #### Turning Separators On and Off 217 | 218 | You can easily hide separators for any rows that are added to an `AloeStackView`: 219 | 220 | ```swift 221 | stackView.hidesSeparatorsByDefault = true 222 | ``` 223 | ![Add rows](Docs/Images/hide_separators_by_default.png) 224 | 225 | The `hidesSeparatorsByDefault` property only applies to new rows that are added. Rows already in the 226 | `AloeStackView` won't be affected. 227 | 228 | You can hide or show separators for existing rows with the `hideSeparator(forRow:)`, 229 | `hideSeparators(forRows:)`, `showSeparator(forRow:)`, and `showSeparators(forRows:)` methods. 230 | 231 | `AloeStackView` also provides a convenient property to automatically hide the last separator: 232 | 233 | ```swift 234 | stackView.automaticallyHidesLastSeparator = true 235 | ``` 236 | ![Add rows](Docs/Images/hide_last_separator.png) 237 | 238 | #### Customizing Separators 239 | 240 | You can change the spacing on the left and right of separators: 241 | 242 | ```swift 243 | stackView.separatorInset = .zero 244 | ``` 245 | ![Add rows](Docs/Images/zero_separator_inset.png) 246 | 247 | In vertical orientation, only the left and right properties of `separatorInset` are used. 248 | 249 | In horizontal orientation, separators are displayed vertically between rows. In this case, only the top and bottom 250 | properties of `separatorInset` are used, and they control the spacing on the top and bottom of separators. 251 | 252 | As with `hidesSeparatorsByDefault`, the `separatorInset` property only applies to new rows that are added. 253 | Rows already in the `AloeStackView` won't be affected. 254 | 255 | You can change the separator inset for existing rows with the `setSeparatorInset(forRow:)` and 256 | `setSeparatorInset(forRows:)` methods. 257 | 258 | `AloeStackView` also provides properties for customizing the color and width (or thickness) of separators: 259 | 260 | ```swift 261 | stackView.separatorColor = .blue 262 | stackView.separatorWidth = 2 263 | ``` 264 | ![Add rows](Docs/Images/large_blue_separators.png) 265 | 266 | These properties affect all of the separators in the `AloeStackView`. 267 | 268 | ### Extending AloeStackView 269 | 270 | `AloeStackView` is an open class, so it's easy to subclass to add custom functionality without changing the original 271 | source code. Additionally, `AloeStackView` provides two methods that can be used to further extend its capabilities. 272 | 273 | #### configureCell(_:) 274 | 275 | Every row in an `AloeStackView` is wrapped in a `UIView` subclass called `StackViewCell`. This view is used for 276 | per-row bookkeeping and also manages UI such as separators and insets. 277 | 278 | Whenever a row is added or inserted into an `AloeStackView`, the `configureCell(_:)` method is called. This 279 | method is passed the newly created `StackViewCell` for the row. 280 | 281 | You can override this method to perform any customization of cells as needed, for example to support custom 282 | features you've added to `AloeStackView` or control the appearance of rows on the screen. 283 | 284 | This method is always called after any default values for the cell have been set, so any changes you make in this 285 | method won't be overwritten by the system. 286 | 287 | #### cellForRow(_:) 288 | 289 | Whenever a row is inserted into an `AloeStackView`, the `cellForRow(_:)` method is called to obtain a new cell for 290 | the row. By default, `cellForRow(_:)` simply returns a new `StackViewCell` that contains the row passed in. 291 | 292 | `StackViewCell`, however, is an open class that can be subclassed to add custom behavior and functionality as 293 | needed. To have `AloeStackView` use your custom cell, override `cellForRow(_:)` and return an instance of your 294 | custom subclass. 295 | 296 | Providing a custom `StackViewCell` subclass allows much more find-grained control over how rows are displayed. It 297 | also allows custom data to be stored along with each row, which can be useful to support any functionality you add to 298 | `AloeStackView`. 299 | 300 | One thing to remember is that `AloeStackView` will apply default values to a cell after it is returned from 301 | `cellForRow(_:)`. Hence, if you need to apply any further customizations to your cell, you should consider doing it in 302 | `configureCell(_:)`. 303 | 304 | #### When to Extend AloeStackView 305 | 306 | These methods together provide quite a lot of flexibility for extending `AloeStackView` to add custom behavior and 307 | functionality. 308 | 309 | For example, you can add new methods to `AloeStackView` to control the way rows are managed, or to support new 310 | types of user interaction. You can customize properties on `StackViewCell` to control the individual appearance of 311 | each row. You can subclass `StackViewCell` to store new data and properties with each row in order to support 312 | custom features you add. Subclassing `StackViewCell` also provides more fine-grained control over how rows are 313 | displayed. 314 | 315 | However, this flexibility inevitably comes with a trade-off in terms of complexity and maintenance. `AloeStackView` 316 | has a comprehensive API that can support a wide variety of use cases out-of-the-box. Hence, it's often better to see if 317 | the behavior you need is available through an existing API before resorting to extending the class to add new features. 318 | This can often save time and effort, both in terms of the cost of developing custom functionality as well as ongoing 319 | maintenance. 320 | 321 | ### When to use AloeStackView 322 | 323 | #### The Short Answer 324 | 325 | `AloeStackView` is best used for shorter screens with less than a screenful or two of content. It is particularly suited to 326 | screens that accept user input, implement forms, or are comprised of a heterogeneous set of views. 327 | 328 | However, it's also helpful to dig a bit deeper into the technical details of `AloeStackView`, as this can help develop a 329 | better understanding of appropriate use cases. 330 | 331 | #### More Details 332 | 333 | `AloeStackView` is a very useful tool to have in the toolbox. Its straightforward, flexible API allows you to build UI 334 | quickly and easily. 335 | 336 | Unlike `UITableView` and `UICollectionView`, you can keep strong references to views in an `AloeStackView` and 337 | make changes to them at any point. This will automatically update the entire UI thanks to Auto Layout - there is no 338 | need to notify `AloeStackView` of the changes. 339 | 340 | This makes `AloeStackView` great for use cases such as forms and screens that take user input. In these situations, 341 | it's often convenient to keep a strong reference to the fields a user is editing, and directly update the UI with validation 342 | feedback. 343 | 344 | `AloeStackView` has no `reloadData` method, or any way to notify it about changes to your views. This makes it less 345 | error-prone and easier to debug than a class like `UITableView`. For example, `AloeStackView` won't crash if not 346 | notified of changes to the underlying data of the views it manages. 347 | 348 | Since `AloeStackView` uses `UIStackView` under the hood, it doesn't recycle views as you scroll. This eliminates 349 | common bugs caused by not recycling views correctly. You also don't need to independently maintain the state of 350 | views as the user interacts with them, which makes it simpler to implement certain kinds of UI. 351 | 352 | However, `AloeStackView` is not suitable in all situations. `AloeStackView` lays out the entire UI in a single pass 353 | when your screen loads. As such, longer screens will start seeing a noticeable delay before the UI is displayed for the 354 | first time. This is not a great experience for users and can make an app feel unresponsive to navigation actions. 355 | Hence, `AloeStackView` should not be used when implementing UI with more than a screenful or two of content. 356 | 357 | Forgoing view recycling is also a trade-off: while `AloeStackView` is faster to write UI with and less error-prone, it will 358 | perform worse and use more memory for longer screens than a class like `UITableView`. Hence, `AloeStackView` is 359 | generally not appropriate for screens that contain many views of the same type, all showing similar data. Classes like 360 | `UITableView` or `UICollectionView` often perform better in those situations. 361 | 362 | ## Installation 363 | 364 | `AloeStackView` can be installed with [Carthage](https://github.com/Carthage/Carthage). Simply add 365 | `github "marlimox/AloeStackView"` to your Cartfile. 366 | 367 | `AloeStackView` can be installed with [CocoaPods](http://cocoapods.org). Simply add 368 | `pod 'AloeStackView'` to your Podfile. 369 | 370 | ## Contributions 371 | 372 | `AloeStackView` is feature complete for the use cases it was originally designed to address. However, UI 373 | development on iOS is never a solved problem, and we expect new use cases to arise and old bugs to be uncovered. 374 | 375 | As such we fully welcome contributions, including new features, feature requests, bug reports, and fixes. If you'd like 376 | to contribute, simply push a PR with a description of your changes. You can also file a GitHub Issue for any bug 377 | reports or feature requests. 378 | 379 | Please feel free to email the project maintainers if you'd like to get in touch. We'd love to hear from you if you or your 380 | company has found this library useful! 381 | 382 | ## Maintainers 383 | 384 | `AloeStackView` is developed and maintained by: 385 | 386 | [Marli Oshlack](https://github.com/marlimox) (marli@oshlack.com) 387 | 388 | [Arthur Pang](https://github.com/apang42) 389 | 390 | ## Contributors 391 | 392 | `AloeStackView` has benefited from the contributions of many other engineers: 393 | 394 | Daniel Crampton, Francisco Diaz, David He, Jeff Hodnett, Eric Horacek, Garrett Larson, Jasmine Lee, Isaac Lim, 395 | Jacky Lu, Noah Martin, Phil Nachum, Gonzalo Nuñez, Laura Skelton, Cal Stephens, and Ortal Yahdav 396 | 397 | In addition, open sourcing this project wouldn't have been possible without the help and support of Jordan Harband, 398 | Tyler Hedrick, Michael Bachand, Laura Skelton, Dan Federman, and John Pottebaum. 399 | 400 | ## License 401 | 402 | `AloeStackView` is released under the Apache License 2.0. See LICENSE for details. 403 | 404 | ## Why is it called AloeStackView? 405 | 406 | We like succulents and find the name soothing 😉 407 | -------------------------------------------------------------------------------- /Sources/AloeStackView/AloeStackView.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 11/10/16. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import UIKit 17 | 18 | /** 19 | * A simple class for laying out a collection of views with a convenient API, while leveraging the 20 | * power of Auto Layout. 21 | */ 22 | open class AloeStackView: UIScrollView { 23 | 24 | // MARK: Lifecycle 25 | 26 | public init() { 27 | super.init(frame: .zero) 28 | setUpViews() 29 | setUpConstraints() 30 | } 31 | 32 | required public init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | // MARK: - Public 37 | 38 | // MARK: Configuring AloeStackView 39 | 40 | /// The direction that rows are laid out in the stack view. 41 | /// 42 | /// If `axis` is `.vertical`, rows will be laid out in a vertical column. If `axis` is 43 | /// `.horizontal`, rows will be laid out horizontally, side-by-side. 44 | /// 45 | /// This property also controls the direction of scrolling in the stack view. If `axis` is 46 | /// `.vertical`, the stack view will scroll vertically, and rows will stretch to fill the width of 47 | /// the stack view. If `axis` is `.horizontal`, the stack view will scroll horizontally, and rows 48 | /// will be sized to fill the height of the stack view. 49 | /// 50 | /// The default value is `.vertical`. 51 | open var axis: NSLayoutConstraint.Axis { 52 | get { return stackView.axis } 53 | set { 54 | stackView.axis = newValue 55 | updateStackViewAxisConstraint() 56 | for case let cell as StackViewCell in stackView.arrangedSubviews { 57 | cell.separatorAxis = newValue == .horizontal ? .vertical : .horizontal 58 | } 59 | } 60 | } 61 | 62 | // MARK: Adding and Removing Rows 63 | 64 | /// Adds a row to the end of the stack view. 65 | /// 66 | /// If `animated` is `true`, the insertion is animated. 67 | open func addRow(_ row: UIView, animated: Bool = false) { 68 | insertCell(withContentView: row, atIndex: stackView.arrangedSubviews.count, animated: animated) 69 | } 70 | 71 | /// Adds multiple rows to the end of the stack view. 72 | /// 73 | /// If `animated` is `true`, the insertions are animated. 74 | open func addRows(_ rows: [UIView], animated: Bool = false) { 75 | rows.forEach { addRow($0, animated: animated) } 76 | } 77 | 78 | /// Adds a row to the beginning of the stack view. 79 | /// 80 | /// If `animated` is `true`, the insertion is animated. 81 | open func prependRow(_ row: UIView, animated: Bool = false) { 82 | insertCell(withContentView: row, atIndex: 0, animated: animated) 83 | } 84 | 85 | /// Adds multiple rows to the beginning of the stack view. 86 | /// 87 | /// If `animated` is `true`, the insertions are animated. 88 | open func prependRows(_ rows: [UIView], animated: Bool = false) { 89 | rows.reversed().forEach { prependRow($0, animated: animated) } 90 | } 91 | 92 | /// Inserts a row above the specified row in the stack view. 93 | /// 94 | /// If `animated` is `true`, the insertion is animated. 95 | open func insertRow(_ row: UIView, before beforeRow: UIView, animated: Bool = false) { 96 | #if swift(>=5.0) 97 | guard 98 | let cell = beforeRow.superview as? StackViewCell, 99 | let index = stackView.arrangedSubviews.firstIndex(of: cell) else { return } 100 | #else 101 | guard 102 | let cell = beforeRow.superview as? StackViewCell, 103 | let index = stackView.arrangedSubviews.index(of: cell) else { return } 104 | #endif 105 | insertCell(withContentView: row, atIndex: index, animated: animated) 106 | } 107 | 108 | /// Inserts multiple rows above the specified row in the stack view. 109 | /// 110 | /// If `animated` is `true`, the insertions are animated. 111 | open func insertRows(_ rows: [UIView], before beforeRow: UIView, animated: Bool = false) { 112 | rows.forEach { insertRow($0, before: beforeRow, animated: animated) } 113 | } 114 | 115 | /// Inserts a row below the specified row in the stack view. 116 | /// 117 | /// If `animated` is `true`, the insertion is animated. 118 | open func insertRow(_ row: UIView, after afterRow: UIView, animated: Bool = false) { 119 | #if swift(>=5.0) 120 | guard 121 | let cell = afterRow.superview as? StackViewCell, 122 | let index = stackView.arrangedSubviews.firstIndex(of: cell) else { return } 123 | #else 124 | guard 125 | let cell = afterRow.superview as? StackViewCell, 126 | let index = stackView.arrangedSubviews.index(of: cell) else { return } 127 | #endif 128 | insertCell(withContentView: row, atIndex: index + 1, animated: animated) 129 | } 130 | 131 | /// Inserts multiple rows below the specified row in the stack view. 132 | /// 133 | /// If `animated` is `true`, the insertions are animated. 134 | open func insertRows(_ rows: [UIView], after afterRow: UIView, animated: Bool = false) { 135 | _ = rows.reduce(afterRow) { currentAfterRow, row in 136 | insertRow(row, after: currentAfterRow, animated: animated) 137 | return row 138 | } 139 | } 140 | 141 | /// Removes the given row from the stack view. 142 | /// 143 | /// If `animated` is `true`, the removal is animated. 144 | open func removeRow(_ row: UIView, animated: Bool = false) { 145 | if let cell = row.superview as? StackViewCell { 146 | removeCell(cell, animated: animated) 147 | } 148 | } 149 | 150 | /// Removes the given rows from the stack view. 151 | /// 152 | /// If `animated` is `true`, the removals are animated. 153 | open func removeRows(_ rows: [UIView], animated: Bool = false) { 154 | rows.forEach { removeRow($0, animated: animated) } 155 | } 156 | 157 | /// Removes all the rows in the stack view. 158 | /// 159 | /// If `animated` is `true`, the removals are animated. 160 | open func removeAllRows(animated: Bool = false) { 161 | stackView.arrangedSubviews.forEach { view in 162 | if let cell = view as? StackViewCell { 163 | removeRow(cell.contentView, animated: animated) 164 | } 165 | } 166 | } 167 | 168 | // MARK: Accessing Rows 169 | 170 | /// The first row in the stack view. 171 | /// 172 | /// This property is nil if there are no rows in the stack view. 173 | open var firstRow: UIView? { 174 | return (stackView.arrangedSubviews.first as? StackViewCell)?.contentView 175 | } 176 | 177 | /// The last row in the stack view. 178 | /// 179 | /// This property is nil if there are no rows in the stack view. 180 | open var lastRow: UIView? { 181 | return (stackView.arrangedSubviews.last as? StackViewCell)?.contentView 182 | } 183 | 184 | /// Returns an array containing of all the rows in the stack view. 185 | /// 186 | /// The rows in the returned array are in the order they appear visually in the stack view. 187 | open func getAllRows() -> [UIView] { 188 | var rows: [UIView] = [] 189 | stackView.arrangedSubviews.forEach { cell in 190 | if let cell = cell as? StackViewCell { 191 | rows.append(cell.contentView) 192 | } 193 | } 194 | return rows 195 | } 196 | 197 | /// Returns `true` if the given row is present in the stack view, `false` otherwise. 198 | open func containsRow(_ row: UIView) -> Bool { 199 | guard let cell = row.superview as? StackViewCell else { return false } 200 | return stackView.arrangedSubviews.contains(cell) 201 | } 202 | 203 | // MARK: Hiding and Showing Rows 204 | 205 | /// Hides the given row, making it invisible. 206 | /// 207 | /// If `animated` is `true`, the change is animated. 208 | open func hideRow(_ row: UIView, animated: Bool = false) { 209 | setRowHidden(row, isHidden: true, animated: animated) 210 | } 211 | 212 | /// Hides the given rows, making them invisible. 213 | /// 214 | /// If `animated` is `true`, the changes are animated. 215 | open func hideRows(_ rows: [UIView], animated: Bool = false) { 216 | rows.forEach { hideRow($0, animated: animated) } 217 | } 218 | 219 | /// Shows the given row, making it visible. 220 | /// 221 | /// If `animated` is `true`, the change is animated. 222 | open func showRow(_ row: UIView, animated: Bool = false) { 223 | setRowHidden(row, isHidden: false, animated: animated) 224 | } 225 | 226 | /// Shows the given rows, making them visible. 227 | /// 228 | /// If `animated` is `true`, the changes are animated. 229 | open func showRows(_ rows: [UIView], animated: Bool = false) { 230 | rows.forEach { showRow($0, animated: animated) } 231 | } 232 | 233 | /// Hides the given row if `isHidden` is `true`, or shows the given row if `isHidden` is `false`. 234 | /// 235 | /// If `animated` is `true`, the change is animated. 236 | open func setRowHidden(_ row: UIView, isHidden: Bool, animated: Bool = false) { 237 | guard let cell = row.superview as? StackViewCell, cell.isHidden != isHidden else { return } 238 | 239 | if animated { 240 | UIView.animate(withDuration: 0.3) { 241 | cell.isHidden = isHidden 242 | cell.layoutIfNeeded() 243 | } 244 | } else { 245 | cell.isHidden = isHidden 246 | } 247 | } 248 | 249 | /// Hides the given rows if `isHidden` is `true`, or shows the given rows if `isHidden` is 250 | /// `false`. 251 | /// 252 | /// If `animated` is `true`, the change are animated. 253 | open func setRowsHidden(_ rows: [UIView], isHidden: Bool, animated: Bool = false) { 254 | rows.forEach { setRowHidden($0, isHidden: isHidden, animated: animated) } 255 | } 256 | 257 | /// Returns `true` if the given row is hidden, `false` otherwise. 258 | open func isRowHidden(_ row: UIView) -> Bool { 259 | return (row.superview as? StackViewCell)?.isHidden ?? false 260 | } 261 | 262 | // MARK: Handling User Interaction 263 | 264 | /// Sets a closure that will be called when the given row in the stack view is tapped by the user. 265 | /// 266 | /// The handler will be passed the row. 267 | open func setTapHandler(forRow row: RowView, handler: ((RowView) -> Void)?) { 268 | guard let cell = row.superview as? StackViewCell else { return } 269 | 270 | if let handler = handler { 271 | cell.tapHandler = { contentView in 272 | guard let contentView = contentView as? RowView else { return } 273 | handler(contentView) 274 | } 275 | } else { 276 | cell.tapHandler = nil 277 | } 278 | } 279 | 280 | // MARK: Styling Rows 281 | 282 | /// The background color of rows in the stack view. 283 | /// 284 | /// This background color will be used for any new row that is added to the stack view. 285 | /// The default color is clear. 286 | open var rowBackgroundColor = UIColor.clear 287 | 288 | /// The highlight background color of rows in the stack view. 289 | /// 290 | /// This highlight background color will be used for any new row that is added to the stack view. 291 | /// The default color is #D9D9D9 (RGB 217, 217, 217). 292 | open var rowHighlightColor = AloeStackView.defaultRowHighlightColor 293 | 294 | /// Sets the background color for the given row to the `UIColor` provided. 295 | open func setBackgroundColor(forRow row: UIView, color: UIColor) { 296 | (row.superview as? StackViewCell)?.rowBackgroundColor = color 297 | } 298 | 299 | /// Sets the background color for the given rows to the `UIColor` provided. 300 | open func setBackgroundColor(forRows rows: [UIView], color: UIColor) { 301 | rows.forEach { setBackgroundColor(forRow: $0, color: color) } 302 | } 303 | 304 | /// Specifies the default inset of rows. 305 | /// 306 | /// This inset will be used for any new row that is added to the stack view. 307 | /// 308 | /// You can use this property to add space between a row and the left and right edges of the stack 309 | /// view and the rows above and below it. Positive inset values move the row inward and away 310 | /// from the stack view edges and away from rows above and below. 311 | /// 312 | /// The default inset is 15pt on each side and 12pt on the top and bottom. 313 | open var rowInset = UIEdgeInsets( 314 | top: 12, 315 | left: AloeStackView.defaultSeparatorInset.left, 316 | bottom: 12, 317 | // Intentional, to match the default spacing of UITableView's cell separators but balanced on 318 | // each side. 319 | right: AloeStackView.defaultSeparatorInset.left) 320 | 321 | /// Sets the inset for the given row to the `UIEdgeInsets` provided. 322 | open func setInset(forRow row: UIView, inset: UIEdgeInsets) { 323 | (row.superview as? StackViewCell)?.rowInset = inset 324 | } 325 | 326 | /// Sets the inset for the given rows to the `UIEdgeInsets` provided. 327 | open func setInset(forRows rows: [UIView], inset: UIEdgeInsets) { 328 | rows.forEach { setInset(forRow: $0, inset: inset) } 329 | } 330 | 331 | // MARK: Styling Separators 332 | 333 | /// The color of separators in the stack view. 334 | /// 335 | /// The default color matches the default color of separators in `UITableView`. 336 | open var separatorColor = AloeStackView.defaultSeparatorColor { 337 | didSet { 338 | for case let cell as StackViewCell in stackView.arrangedSubviews { 339 | cell.separatorColor = separatorColor 340 | } 341 | } 342 | } 343 | 344 | /// The width (or thickness) of separators in the stack view. 345 | /// 346 | /// The default width is 1px. 347 | open var separatorWidth: CGFloat = 1 / UIScreen.main.scale { 348 | didSet { 349 | for case let cell as StackViewCell in stackView.arrangedSubviews { 350 | cell.separatorWidth = separatorWidth 351 | } 352 | } 353 | } 354 | 355 | /// The height of separators in the stack view. 356 | /// 357 | /// This property is the same as `separatorWidth` and is maintained for backwards compatibility. 358 | /// 359 | /// The default height is 1px. 360 | open var separatorHeight: CGFloat { 361 | get { return separatorWidth } 362 | set { separatorWidth = newValue } 363 | } 364 | 365 | /// Specifies the default inset of row separators. 366 | /// 367 | /// Only left and right insets are honored when `axis` is `.vertical`, and only top and bottom 368 | /// insets are honored when `axis` is `.horizontal`. This inset will be used for any new row that 369 | /// is added to the stack view. The default left and right insets match the default inset of cell 370 | /// separators in `UITableView`, which are 15pt on the left and 0pt on the right. The default top 371 | /// and bottom insets are 0pt. 372 | open var separatorInset: UIEdgeInsets = AloeStackView.defaultSeparatorInset 373 | 374 | /// Sets the separator inset for the given row to the `UIEdgeInsets` provided. 375 | /// 376 | /// Only left and right insets are honored. 377 | open func setSeparatorInset(forRow row: UIView, inset: UIEdgeInsets) { 378 | (row.superview as? StackViewCell)?.separatorInset = inset 379 | } 380 | 381 | /// Sets the separator inset for the given rows to the `UIEdgeInsets` provided. 382 | /// 383 | /// Only left and right insets are honored. 384 | open func setSeparatorInset(forRows rows: [UIView], inset: UIEdgeInsets) { 385 | rows.forEach { setSeparatorInset(forRow: $0, inset: inset) } 386 | } 387 | 388 | // MARK: Hiding and Showing Separators 389 | 390 | /// Specifies the default visibility of row separators. 391 | /// 392 | /// When `true`, separators will be hidden for any new rows added to the stack view. 393 | /// When `false, separators will be visible for any new rows added. Default is `false`, meaning 394 | /// separators are visible for any new rows that are added. 395 | open var hidesSeparatorsByDefault = false 396 | 397 | /// Hides the separator for the given row. 398 | open func hideSeparator(forRow row: UIView) { 399 | if let cell = row.superview as? StackViewCell { 400 | cell.shouldHideSeparator = true 401 | updateSeparatorVisibility(forCell: cell) 402 | } 403 | } 404 | 405 | /// Hides separators for the given rows. 406 | open func hideSeparators(forRows rows: [UIView]) { 407 | rows.forEach { hideSeparator(forRow: $0) } 408 | } 409 | 410 | /// Shows the separator for the given row. 411 | open func showSeparator(forRow row: UIView) { 412 | if let cell = row.superview as? StackViewCell { 413 | cell.shouldHideSeparator = false 414 | updateSeparatorVisibility(forCell: cell) 415 | } 416 | } 417 | 418 | /// Shows separators for the given rows. 419 | open func showSeparators(forRows rows: [UIView]) { 420 | rows.forEach { showSeparator(forRow: $0) } 421 | } 422 | 423 | /// Automatically hides the separator of the last cell in the stack view. 424 | /// 425 | /// Default is `false`. 426 | open var automaticallyHidesLastSeparator = false { 427 | didSet { 428 | if let cell = stackView.arrangedSubviews.last as? StackViewCell { 429 | updateSeparatorVisibility(forCell: cell) 430 | } 431 | } 432 | } 433 | 434 | // MARK: Modifying the Scroll Position 435 | 436 | /// Scrolls the given row onto screen so that it is fully visible. 437 | /// 438 | /// If `animated` is `true`, the scroll is animated. If the row is already fully visible, this 439 | /// method does nothing. 440 | open func scrollRowToVisible(_ row: UIView, animated: Bool = true) { 441 | guard let superview = row.superview else { return } 442 | scrollRectToVisible(convert(row.frame, from: superview), animated: animated) 443 | } 444 | 445 | // MARK: Extending AloeStackView 446 | 447 | /// Returns the `StackViewCell` to be used for the given row. 448 | /// 449 | /// An instance of `StackViewCell` wraps every row in the stack view. 450 | /// 451 | /// Subclasses can override this method to return a custom `StackViewCell` subclass, for example 452 | /// to add custom behavior or functionality that is not provided by default. 453 | /// 454 | /// If you customize the values of some properties of `StackViewCell` in this method, these values 455 | /// may be overwritten by default values after the cell is returned. To customize the values of 456 | /// properties of the cell, override `configureCell(_:)` and perform the customization there, 457 | /// rather than on the cell returned from this method. 458 | open func cellForRow(_ row: UIView) -> StackViewCell { 459 | return StackViewCell(contentView: row) 460 | } 461 | 462 | /// Allows subclasses to configure the properties of the given `StackViewCell`. 463 | /// 464 | /// This method is called for newly created cells after the default values of any properties of 465 | /// the cell have been set by the superclass. 466 | /// 467 | /// The default implementation of this method does nothing. 468 | open func configureCell(_ cell: StackViewCell) { } 469 | 470 | // MARK: - Private 471 | 472 | private let stackView = UIStackView() 473 | 474 | private var stackViewAxisConstraint: NSLayoutConstraint? 475 | 476 | private func setUpViews() { 477 | setUpSelf() 478 | setUpStackView() 479 | } 480 | 481 | private func setUpSelf() { 482 | backgroundColor = UIColor.white 483 | } 484 | 485 | private func setUpStackView() { 486 | stackView.translatesAutoresizingMaskIntoConstraints = false 487 | stackView.axis = .vertical 488 | addSubview(stackView) 489 | } 490 | 491 | private func setUpConstraints() { 492 | setUpStackViewConstraints() 493 | updateStackViewAxisConstraint() 494 | } 495 | 496 | private func setUpStackViewConstraints() { 497 | NSLayoutConstraint.activate([ 498 | stackView.topAnchor.constraint(equalTo: topAnchor), 499 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor), 500 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor), 501 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor), 502 | ]) 503 | } 504 | 505 | private func updateStackViewAxisConstraint() { 506 | stackViewAxisConstraint?.isActive = false 507 | if stackView.axis == .vertical { 508 | stackViewAxisConstraint = stackView.widthAnchor.constraint(equalTo: widthAnchor) 509 | } else { 510 | stackViewAxisConstraint = stackView.heightAnchor.constraint(equalTo: heightAnchor) 511 | } 512 | stackViewAxisConstraint?.isActive = true 513 | } 514 | 515 | private func createCell(withContentView contentView: UIView) -> StackViewCell { 516 | let cell = cellForRow(contentView) 517 | 518 | cell.rowBackgroundColor = rowBackgroundColor 519 | cell.rowHighlightColor = rowHighlightColor 520 | cell.rowInset = rowInset 521 | cell.separatorAxis = axis == .horizontal ? .vertical : .horizontal 522 | cell.separatorColor = separatorColor 523 | cell.separatorHeight = separatorHeight 524 | cell.separatorInset = separatorInset 525 | cell.shouldHideSeparator = hidesSeparatorsByDefault 526 | 527 | configureCell(cell) 528 | 529 | return cell 530 | } 531 | 532 | private func insertCell(withContentView contentView: UIView, atIndex index: Int, animated: Bool) { 533 | let cellToRemove = containsRow(contentView) ? contentView.superview : nil 534 | 535 | let cell = createCell(withContentView: contentView) 536 | stackView.insertArrangedSubview(cell, at: index) 537 | 538 | if let cellToRemove = cellToRemove as? StackViewCell { 539 | removeCell(cellToRemove, animated: false) 540 | } 541 | 542 | updateSeparatorVisibility(forCell: cell) 543 | 544 | // A cell can affect the visibility of the cell before it, e.g. if 545 | // `automaticallyHidesLastSeparator` is true and a new cell is added as the last cell, so update 546 | // the previous cell's separator visibility as well. 547 | if let cellAbove = cellAbove(cell: cell) { 548 | updateSeparatorVisibility(forCell: cellAbove) 549 | } 550 | 551 | if animated { 552 | cell.alpha = 0 553 | layoutIfNeeded() 554 | UIView.animate(withDuration: 0.3) { 555 | cell.alpha = 1 556 | } 557 | } 558 | } 559 | 560 | private func removeCell(_ cell: StackViewCell, animated: Bool) { 561 | let aboveCell = cellAbove(cell: cell) 562 | 563 | let completion: (Bool) -> Void = { [weak self] _ in 564 | guard let `self` = self else { return } 565 | cell.removeFromSuperview() 566 | 567 | // When removing a cell, the cell before the removed cell is the only cell whose separator 568 | // visibility could be affected, so we need to update its visibility. 569 | if let aboveCell = aboveCell { 570 | self.updateSeparatorVisibility(forCell: aboveCell) 571 | } 572 | } 573 | 574 | if animated { 575 | UIView.animate( 576 | withDuration: 0.3, 577 | animations: { 578 | cell.isHidden = true 579 | }, 580 | completion: completion) 581 | } else { 582 | completion(true) 583 | } 584 | } 585 | 586 | private func updateSeparatorVisibility(forCell cell: StackViewCell) { 587 | let isLastCellAndHidingIsEnabled = automaticallyHidesLastSeparator && 588 | cell === stackView.arrangedSubviews.last 589 | let cellConformsToSeparatorHiding = cell.contentView is SeparatorHiding 590 | 591 | cell.isSeparatorHidden = 592 | isLastCellAndHidingIsEnabled || 593 | cellConformsToSeparatorHiding || 594 | cell.shouldHideSeparator 595 | } 596 | 597 | private func cellAbove(cell: StackViewCell) -> StackViewCell? { 598 | #if swift(>=5.0) 599 | guard let index = stackView.arrangedSubviews.firstIndex(of: cell), index > 0 else { return nil } 600 | #else 601 | guard let index = stackView.arrangedSubviews.index(of: cell), index > 0 else { return nil } 602 | #endif 603 | return stackView.arrangedSubviews[index - 1] as? StackViewCell 604 | } 605 | 606 | private static let defaultRowHighlightColor: UIColor = UIColor(red: 217 / 255, green: 217 / 255, blue: 217 / 255, alpha: 1) 607 | private static let defaultSeparatorColor: UIColor = UITableView().separatorColor ?? .clear 608 | private static let defaultSeparatorInset: UIEdgeInsets = UITableView().separatorInset 609 | 610 | } 611 | -------------------------------------------------------------------------------- /Sources/AloeStackView/AloeStackViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Fan Cox on 11/30/16. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import UIKit 17 | 18 | /** 19 | * A view controller that specializes in managing an AloeStackView. 20 | */ 21 | open class AloeStackViewController: UIViewController { 22 | 23 | // MARK: Lifecycle 24 | 25 | public init() { 26 | super.init(nibName: nil, bundle: nil) 27 | } 28 | 29 | public required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | open override func loadView() { 34 | view = stackView 35 | } 36 | 37 | open override func viewDidAppear(_ animated: Bool) { 38 | super.viewDidAppear(animated) 39 | if automaticallyFlashScrollIndicators { 40 | stackView.flashScrollIndicators() 41 | } 42 | } 43 | 44 | // MARK: Public 45 | 46 | /// The stack view this controller manages. 47 | public let stackView = AloeStackView() 48 | 49 | /// When true, automatically displays the scroll indicators in the stack view momentarily whenever the view appears. 50 | /// 51 | /// Default is `false`. 52 | open var automaticallyFlashScrollIndicators = false 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/AloeStackView/Protocols/Highlightable.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 11/14/16. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import UIKit 17 | 18 | /** 19 | * Indicates that a row in an `AloeStackView` should be highlighted when the user touches it. 20 | * 21 | * Rows that are added to an `AloeStackView` can conform to this protocol to have their 22 | * background color automatically change to a highlighted color (or some other custom behavior defined by the row) when the user is pressing down on 23 | * them. 24 | */ 25 | public protocol Highlightable { 26 | 27 | /// Checked when the user touches down on a row to determine if the row should be highlighted. 28 | /// 29 | /// The default implementation of this method always returns `true`. 30 | var isHighlightable: Bool { get } 31 | 32 | /// Called when the highlighted state of the row changes. 33 | /// Override this method to provide custom highlighting behavior for the row. 34 | /// 35 | /// The default implementation of this method changes the background color of the row to the `rowHighlightColor`. 36 | func setIsHighlighted(_ isHighlighted: Bool) 37 | 38 | } 39 | 40 | extension Highlightable where Self: UIView { 41 | 42 | public var isHighlightable: Bool { 43 | return true 44 | } 45 | 46 | public func setIsHighlighted(_ isHighlighted: Bool) { 47 | guard let cell = superview as? StackViewCell else { return } 48 | cell.backgroundColor = isHighlighted ? cell.rowHighlightColor : cell.rowBackgroundColor 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/AloeStackView/Protocols/SeparatorHiding.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 2/7/17. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | /** 17 | * Indicates that a row in an `AloeStackView` should hide its separator. 18 | * 19 | * Rows that are added to an `AloeStackView` can conform to this protocol to automatically their 20 | * separators. 21 | * 22 | * This behavior can be useful when implementing shared, reusable rows that should always have this 23 | * behavior when they are used in an `AloeStackView`. 24 | */ 25 | public protocol SeparatorHiding { 26 | } 27 | -------------------------------------------------------------------------------- /Sources/AloeStackView/Protocols/Tappable.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 11/14/16. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | /** 17 | * Notifies a row in an `AloeStackView` when it receives a user tap. 18 | * 19 | * Rows that are added to an `AloeStackView` can conform to this protocol to be notified when a 20 | * user taps on the row. This notification happens regardless of whether the row has a tap handler 21 | * set for it or not. 22 | * 23 | * This notification can be used to implement default behavior in a view that should always happen 24 | * when that view is tapped. 25 | */ 26 | public protocol Tappable { 27 | 28 | /// Called when the row is tapped by the user. 29 | func didTapView() 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/AloeStackView/Views/SeparatorView.swift: -------------------------------------------------------------------------------- 1 | // Created by Arthur Pang on 9/22/16. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import UIKit 17 | 18 | internal final class SeparatorView: UIView { 19 | 20 | // MARK: Lifecycle 21 | 22 | internal init() { 23 | super.init(frame: .zero) 24 | translatesAutoresizingMaskIntoConstraints = false 25 | setUpConstraints() 26 | } 27 | 28 | internal required init?(coder aDecoder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | // MARK: Internal 33 | 34 | internal override var intrinsicContentSize: CGSize { 35 | return CGSize(width: width, height: width) 36 | } 37 | 38 | internal var color: UIColor { 39 | get { return backgroundColor ?? .clear } 40 | set { backgroundColor = newValue } 41 | } 42 | 43 | internal var width: CGFloat = 1 { 44 | didSet { invalidateIntrinsicContentSize() } 45 | } 46 | 47 | // MARK: Private 48 | 49 | private func setUpConstraints() { 50 | setContentHuggingPriority(.defaultLow, for: .horizontal) 51 | setContentHuggingPriority(.defaultLow, for: .vertical) 52 | setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 53 | setContentCompressionResistancePriority(.defaultLow, for: .vertical) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/AloeStackView/Views/StackViewCell.swift: -------------------------------------------------------------------------------- 1 | // Created by Marli Oshlack on 11/1/16. 2 | // Copyright Marli Oshlack 2018. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import UIKit 17 | 18 | /** 19 | * A view that wraps every row in a stack view. 20 | */ 21 | open class StackViewCell: UIView { 22 | 23 | // MARK: Lifecycle 24 | 25 | public init(contentView: UIView) { 26 | self.contentView = contentView 27 | 28 | super.init(frame: .zero) 29 | translatesAutoresizingMaskIntoConstraints = false 30 | if #available(iOS 11.0, *) { 31 | insetsLayoutMarginsFromSafeArea = false 32 | } 33 | 34 | setUpViews() 35 | setUpConstraints() 36 | setUpTapGestureRecognizer() 37 | } 38 | 39 | public required init?(coder aDecoder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | // MARK: Open 44 | 45 | open override var isHidden: Bool { 46 | didSet { 47 | guard isHidden != oldValue else { return } 48 | separatorView.alpha = isHidden ? 0 : 1 49 | } 50 | } 51 | 52 | open var rowHighlightColor = UIColor(red: 217 / 255, green: 217 / 255, blue: 217 / 255, alpha: 1) 53 | 54 | open var rowBackgroundColor = UIColor.clear { 55 | didSet { backgroundColor = rowBackgroundColor } 56 | } 57 | 58 | open var rowInset: UIEdgeInsets { 59 | get { return layoutMargins } 60 | set { layoutMargins = newValue } 61 | } 62 | 63 | open var separatorAxis: NSLayoutConstraint.Axis = .horizontal { 64 | didSet { 65 | updateSeparatorAxisConstraints() 66 | updateSeparatorInset() 67 | } 68 | } 69 | 70 | open var separatorColor: UIColor { 71 | get { return separatorView.color } 72 | set { separatorView.color = newValue } 73 | } 74 | 75 | open var separatorWidth: CGFloat { 76 | get { return separatorView.width } 77 | set { separatorView.width = newValue } 78 | } 79 | 80 | /// Alias for `separatorWidth`. Maintained for backwards compatibility. 81 | open var separatorHeight: CGFloat { 82 | get { return separatorWidth } 83 | set { separatorWidth = newValue } 84 | } 85 | 86 | open var separatorInset: UIEdgeInsets = .zero { 87 | didSet { updateSeparatorInset() } 88 | } 89 | 90 | open var isSeparatorHidden: Bool { 91 | get { return separatorView.isHidden } 92 | set { separatorView.isHidden = newValue } 93 | } 94 | 95 | // MARK: Public 96 | 97 | public let contentView: UIView 98 | 99 | // MARK: UIResponder 100 | 101 | open override func touchesBegan(_ touches: Set, with event: UIEvent?) { 102 | super.touchesBegan(touches, with: event) 103 | guard contentView.isUserInteractionEnabled else { return } 104 | 105 | if let contentView = contentView as? Highlightable, contentView.isHighlightable { 106 | contentView.setIsHighlighted(true) 107 | } 108 | } 109 | 110 | open override func touchesMoved(_ touches: Set, with event: UIEvent?) { 111 | super.touchesMoved(touches, with: event) 112 | guard contentView.isUserInteractionEnabled, let touch = touches.first else { return } 113 | 114 | let locationInSelf = touch.location(in: self) 115 | 116 | if let contentView = contentView as? Highlightable, contentView.isHighlightable { 117 | let isPointInsideCell = point(inside: locationInSelf, with: event) 118 | contentView.setIsHighlighted(isPointInsideCell) 119 | } 120 | } 121 | 122 | open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 123 | super.touchesCancelled(touches, with: event) 124 | guard contentView.isUserInteractionEnabled else { return } 125 | 126 | if let contentView = contentView as? Highlightable, contentView.isHighlightable { 127 | contentView.setIsHighlighted(false) 128 | } 129 | } 130 | 131 | open override func touchesEnded(_ touches: Set, with event: UIEvent?) { 132 | super.touchesEnded(touches, with: event) 133 | guard contentView.isUserInteractionEnabled else { return } 134 | 135 | if let contentView = contentView as? Highlightable, contentView.isHighlightable { 136 | contentView.setIsHighlighted(false) 137 | } 138 | } 139 | 140 | // MARK: Internal 141 | 142 | internal var tapHandler: ((UIView) -> Void)? { 143 | didSet { updateTapGestureRecognizerEnabled() } 144 | } 145 | 146 | // Whether the separator should be hidden or not for this cell. Note that this doesn't always 147 | // reflect whether the separator is hidden or not, since, for example, the separator could be 148 | // hidden because it's the last row in the stack view and 149 | // `automaticallyHidesLastSeparator` is `true`. 150 | internal var shouldHideSeparator = false 151 | 152 | // MARK: Private 153 | 154 | private let separatorView = SeparatorView() 155 | private let tapGestureRecognizer = UITapGestureRecognizer() 156 | 157 | private var separatorTopConstraint: NSLayoutConstraint? 158 | private var separatorBottomConstraint: NSLayoutConstraint? 159 | private var separatorLeadingConstraint: NSLayoutConstraint? 160 | private var separatorTrailingConstraint: NSLayoutConstraint? 161 | 162 | private func setUpViews() { 163 | setUpSelf() 164 | setUpContentView() 165 | setUpSeparatorView() 166 | } 167 | 168 | private func setUpSelf() { 169 | clipsToBounds = true 170 | } 171 | 172 | private func setUpContentView() { 173 | contentView.translatesAutoresizingMaskIntoConstraints = false 174 | addSubview(contentView) 175 | } 176 | 177 | private func setUpSeparatorView() { 178 | addSubview(separatorView) 179 | } 180 | 181 | private func setUpConstraints() { 182 | setUpContentViewConstraints() 183 | setUpSeparatorViewConstraints() 184 | updateSeparatorAxisConstraints() 185 | } 186 | 187 | private func setUpContentViewConstraints() { 188 | let bottomConstraint = contentView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) 189 | bottomConstraint.priority = UILayoutPriority(rawValue: UILayoutPriority.required.rawValue - 1) 190 | 191 | NSLayoutConstraint.activate([ 192 | contentView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), 193 | contentView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), 194 | contentView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), 195 | bottomConstraint 196 | ]) 197 | } 198 | 199 | private func setUpSeparatorViewConstraints() { 200 | separatorTopConstraint = separatorView.topAnchor.constraint(equalTo: topAnchor) 201 | separatorBottomConstraint = separatorView.bottomAnchor.constraint(equalTo: bottomAnchor) 202 | separatorLeadingConstraint = separatorView.leadingAnchor.constraint(equalTo: leadingAnchor) 203 | separatorTrailingConstraint = separatorView.trailingAnchor.constraint(equalTo: trailingAnchor) 204 | } 205 | 206 | private func setUpTapGestureRecognizer() { 207 | tapGestureRecognizer.addTarget(self, action: #selector(handleTap(_:))) 208 | tapGestureRecognizer.delegate = self 209 | addGestureRecognizer(tapGestureRecognizer) 210 | updateTapGestureRecognizerEnabled() 211 | } 212 | 213 | @objc private func handleTap(_ tapGestureRecognizer: UITapGestureRecognizer) { 214 | guard contentView.isUserInteractionEnabled else { return } 215 | (contentView as? Tappable)?.didTapView() 216 | tapHandler?(contentView) 217 | } 218 | 219 | private func updateSeparatorAxisConstraints() { 220 | separatorTopConstraint?.isActive = separatorAxis == .vertical 221 | separatorBottomConstraint?.isActive = true 222 | separatorLeadingConstraint?.isActive = separatorAxis == .horizontal 223 | separatorTrailingConstraint?.isActive = true 224 | } 225 | 226 | private func updateSeparatorInset() { 227 | separatorTopConstraint?.constant = separatorInset.top 228 | separatorBottomConstraint?.constant = separatorAxis == .horizontal ? 0 : -separatorInset.bottom 229 | separatorLeadingConstraint?.constant = separatorInset.left 230 | separatorTrailingConstraint?.constant = separatorAxis == .vertical ? 0 : -separatorInset.right 231 | } 232 | 233 | private func updateTapGestureRecognizerEnabled() { 234 | tapGestureRecognizer.isEnabled = contentView is Tappable || tapHandler != nil 235 | } 236 | 237 | } 238 | 239 | extension StackViewCell: UIGestureRecognizerDelegate { 240 | 241 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 242 | guard let view = gestureRecognizer.view else { return false } 243 | 244 | let location = touch.location(in: view) 245 | var hitView = view.hitTest(location, with: nil) 246 | 247 | // Traverse the chain of superviews looking for any UIControls. 248 | while hitView != view && hitView != nil { 249 | if hitView is UIControl { 250 | // Ensure UIControls get the touches instead of the tap gesture. 251 | return false 252 | } 253 | hitView = hitView?.superview 254 | } 255 | 256 | return true 257 | } 258 | 259 | } 260 | -------------------------------------------------------------------------------- /Tests/AloeStackViewTests/AloeStackViewTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright Marli Oshlack 2018. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | @testable import AloeStackView 18 | 19 | final class AloeStackViewTests: XCTestCase { 20 | 21 | func test() { 22 | } 23 | 24 | func testSetRowHiddenDuplicateCalls() { 25 | let stackView = AloeStackView() 26 | let row = UIView() 27 | stackView.addRow(row) 28 | 29 | stackView.setRowHidden(row, isHidden: true, animated: true) 30 | stackView.setRowHidden(row, isHidden: true, animated: true) 31 | stackView.setRowHidden(row, isHidden: false, animated: true) 32 | 33 | XCTAssertFalse(stackView.isRowHidden(row)) 34 | } 35 | 36 | func testInsertedRowIsFirstAndLastRow() { 37 | let stackView = AloeStackView() 38 | let row = UIView() 39 | stackView.addRow(row) 40 | XCTAssertTrue(stackView.firstRow === row) 41 | XCTAssertTrue(stackView.lastRow === row) 42 | } 43 | 44 | func testStackViewHasFirstAndLastRow() { 45 | let stackView = AloeStackView() 46 | let firstRow = UIView() 47 | let middleRow = UILabel() 48 | let lastRow = UIButton() 49 | stackView.addRows([firstRow, middleRow, lastRow]) 50 | XCTAssertTrue(stackView.firstRow === firstRow) 51 | XCTAssertTrue(stackView.lastRow === lastRow) 52 | } 53 | 54 | } 55 | --------------------------------------------------------------------------------