├── .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 | [](https://github.com/Carthage/Carthage)
6 | [](https://cocoapods.org/pods/AloeStackView)
7 | [](https://cocoapods.org/pods/AloeStackView)
8 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------