Stackable is a delightful and declarative set of utilities for UIStackView
. It is designed to make your layout code easier to write, read, and communicate with your designer.
Stackable aims to bridge the gap between the way designers articulate layout and the way developers express that layout in code.
stack.stackable.add([
logo,
30,
"Example Views",
10,
cells
.outset(to: view)
.margins(alignedWith: stack),
UIStackView.stackable.hairlines(around: cells)
.outset(to: view),
20...,
"Copyright Rightpoint",
])
Stackable includes built-in support for all UIView
subclasses.
Add them using stackView.stackable.add(_ stackables: [Stackable])
stack.stackable.add([
imageView,
label,
button,
])
Additionally, you can reference UIViewController
directly to include its view.
stack.stackable.add([
viewController,
button,
])
There is also support for:
String
NSAttributedString
UIImage
stack.stackable.add([
"String",
NSAttributedString(string: "Attributed String"),
UIImage(named: "example"),
])
Views can be expressively manipulated to adjust their alignment in a UIStackView
.
let stack = UIStackView()
stack.axis = .vertical
stack.stackable.add([
imageView
.inset(by: .init(top: 20, left: 20, bottom: 20, right: 20), //imageView padded on all sides 20px
label
.inset(by: .init(top: 0, left: -20, bottom: 0, right: -20) //inset can also be negative to move outside of bounds of `stack`
.aligned(.right), // aligned right (transforms are composable)
button
.aligned(.centerX), // button aligned center horizontally
hairline
.outset(to: view), // hairline pinned to horizontal edges of `view`
cells
.outset(to: view) // cells horizontal edges pinned to horizontal edges of `view`
.margins(alignedWith: view), // cell layout margins updated to line up with view.layoutMarginsGuide
])
Spaces can be added using a number literal or StackableSpaceItem
. Add them alongside your views.
stack.stackable.add([
20, //constant space
viewA,
20, //custom spacing after `viewA`, will be removed if viewA is hidden.
viewB,
12.5, //floating point works too!
viewC,
UIStackView.stackable.constantSpace(20), //Constant space, not dependent on visibility of `viewC`
viewD,
UIStackView.stackable.space(after: viewD, 20), //Explicit custom space dependent on visibility of viewD. Equivalent to just using `20` here.
viewE,
UIStackView.stackable.space(before: viewF, 20), //Custom space before viewF. Dependent on the visibility of `viewF`
viewF,
])
stack.stackable.add([
viewA,
20...30, // Flexible space, at least 20, at most 30, inclusive. Flexible spaces do not track view visibility.
viewB,
20..., // Flexible space. At least 20, no maximum.
viewC,
...15.5, // Flexible space. At least 0, at most 15.5, floating point works too!
viewD,
UIStackView.stackable.flexibleSpace, // flexible space, at least 0, no maximum. Equivalent to `0...`
viewE,
UIStackView.stackable.flexibleSpace(.atLeast(20)), // flexible space, at least 20, no maximum. equivalent to `20...`
viewF,
UIStackView.stackable.flexibleSpace(.atMost(20)), // flexible space. at least 0, at most 20, equivalent to `...20`
viewG,
UIStackView.stackable.flexibleSpace(.range(10...20)), // flexible space, at least 10, at most 20, equivalent to `10...20`
])
let cells: [UIView] = ...
stack.stackable.add([
cells,
UIStackView.stackable.space(afterGroup: cells, 20), // A space that is dependent on at least one view in `cells` to be visible.
])
stack.stackable.add([
viewA,
UIStackView.stackable.hairline, // Simple hairline, extended to edges of StackView.
viewB,
UIStackView.stackable.hairline(after: viewB), // hairline that mirrors visibility of `viewB`
viewC,
UIStackView.stackable.hairline(between: viewC, viewD), // hairline that is dependent on both `viewC` and `viewD` being visible.
viewD,
UIStackView.stackable.hairline(before: viewE), // hairline before `viewE`, mirrors visibility of `viewE`
viewE,
viewF,
UIStackView.stackable.hairlines(around: viewF), // hairline added above and below viewF. Mirrors visibility of `viewF`
])
let cells: [UIView] = ...
stack.stackable.add([
cellsA,
UIStackView.stackable.hairlines(between: cellsA), // Hairlines between any visibile members of `cellsA`
cellsB,
UIStackView.stackable.hairlines(after: cellsB), // Hairlines after all visible members of `cellsB`
cellsC,
UIStackView.stackable.hairlines(around: cellsC), // Hairlines above and below all visible members of `cellsC`
])
stack.stackable.add([
viewA,
UIStackView.hairline
.inset(by: .init(top: 0, left: 10, bottom: 0, right: 10) // hairline inset horizontally by 10
viewB,
UIStackView.hairline
.inset(by: .init(top: 0, left: -10, bottom: 0, right: -10) // hairline outset horizontally by 10
viewC,
UIStackView.hairline
.outset(to: view) // hairline constrained to edges of some ancestor (horizontal edges for a vertical stack)
])
Hairline appearance can be customized on a per hairline or group basis:
stack.stackable.add([
viewA,
UIStackView.hairline
.color(.lightGray)
.thickness(1),
cells,
UIStackView.hairlines(around: cells)
.color(.red)
.thickness(10)
])
It can also be customized on a per UIStackView
basis:
let stack = UIStackView()
stack.stackable.hairlineColor = .lightGray
stack.stackable.hairlineThickness = 1
It can also be customized with a global default:
UIStackView.stackable.hairlineColor = .lightGray
UIStackView.stackable.hairlineThickness = 1
If more customiztion is required, you may provide a HairlineProvider
to vend the UIView
that should be used as a hairline:
let stack = UIStackView()
stack.stackable.hairlineProvider = { stackView in
let customHairline = UIView()
//...
return customHairline
}
// global default provider
UIStackView.stackable.hairlineProvider = { stackView in
let customHairline = UIView()
//...
return customHairline
}
To run the example project, clone the repo, and run pod install
from the Example directory first.
- iOS 12.0+
Stackable is available as a Swift Package. You can also install it through CocoaPods by adding the following line to your Podfile:
pod 'RPStackable'
Stackable is available under the MIT license. See the LICENSE file for more info.