起源是在 github 上看到 kean 的 Stacks (A micro UIStackView convenience API inspired by SwiftUI)
使用 Stacks 可以用声明式语法创建 UIStackView
let stack: UIView = .hStack(alignment: .center, margins: .all(16), [
.vStack(spacing: 8, [
titleLabel,
subtitleLabel
]),
.spacer(minLength: 16),
star
])
但是 Swift 有 resultBulider(functionBuilder),使用它可以更像 SwiftUI 例如这样
let content = VStack {
VStack {
iconImageView.width(.point(136)).height(.point(136))
specLabel
}
.spacing(10)
.alignment(.center)
VStack {
facebookLoginButton.height(.point(48))
googleLoginButton.height(.point(48))
mobileLoginButton.height(.point(48))
if #available(iOS 13.0, *) {
appleLoginButton.height(.point(48))
}
}
.spacing(18)
}
.distribution(.equalSpacing)
在 stacks 的基础上用 resultbuilder 改造
swift 官方文档
A result builder is a type you define that adds syntax for creating nested data, like a list or tree, in a natural, declarative way.
@resultBuilder
struct SettingsBuilder {
static func buildBlock() -> [Setting] { [] }
}
其他
Stacks 源码上有个问题, updateConstraints 的调用位置错了
fileprivate override func updateConstraints() {
super.updateConstraints()
NSLayoutConstraint.deactivate(_constraints)
let attributes: [NSLayoutConstraint.Attribute]
switch axis {
case .horizontal: attributes = [.width]
case .vertical: attributes = [.height]
default: attributes = [.height, .width] // Not really an expected use-case
}
_constraints = attributes.map {
let constraint = NSLayoutConstraint(item: self, attribute: $0, relatedBy: isFixed ? .equal : .greaterThanOrEqual, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: length)
constraint.priority = UILayoutPriority(999)
return constraint
}
NSLayoutConstraint.activate(_constraints)
}
Important: Call
[super updateConstraints]as the final step in your implementation.
使用 resultBuilder(functionBuilder) 封装的过程
protocol StackNode {
var subNodes: [UIView] { get }
}
extension UIView: StackNode {
var subNodes: [UIView] { [self] }
}
struct ComponentNode: StackNode {
let views: [UIView]
var subNodes: [UIView] { views }
}
struct EmptyNode: StackNode {
var subNodes: [UIView] { [] }
}
@resultBuilder
struct StackBuilder {
static func buildBlock(_ component: StackNode) -> StackNode {
return component
}
static func buildBlock(_ components: StackNode...) -> StackNode {
ComponentNode(views: components.flatMap{ $0.subNodes })
}
static func buildBlock(_ components: [StackNode]) -> StackNode {
ComponentNode(views: components.flatMap{ $0.subNodes })
}
static func buildArray(_ components: [StackNode]) -> StackNode {
ComponentNode(views: components.flatMap{ $0.subNodes })
}
static func buildEither(first component: StackNode) -> StackNode {
component
}
static func buildEither(second component: StackNode) -> StackNode {
component
}
static func buildOptional(_ component: StackNode?) -> StackNode {
component ?? EmptyNode()
}
}
final class VStack: UIStackView {
init(@StackBuilder nodes: () -> StackNode) {
super.init(frame: .zero)
axis = .vertical
translatesAutoresizingMaskIntoConstraints = false
nodes().subNodes.forEach { addArrangedSubview($0) }
}
required init(coder: NSCoder) { super.init(coder: coder) }
}
protocol StackModifier {
associatedtype Stack: UIStackView
func alignment(_ alignment: Stack.Alignment) -> Stack
func distribution(_ distribution: Stack.Distribution) -> Stack
func spacing(_ spacing: CGFloat) -> Stack
}
extension VStack: StackModifier {
func alignment(_ alignment: UIStackView.Alignment) -> VStack {
self.alignment = alignment
return self
}
func distribution(_ distribution: UIStackView.Distribution) -> VStack {
self.distribution = distribution
return self
}
func spacing(_ spacing: CGFloat) -> VStack {
self.spacing = spacing
return self
}
}
final class HStack: UIStackView {
init(@StackBuilder nodes: () -> StackNode) {
super.init(frame: .zero)
axis = .horizontal
translatesAutoresizingMaskIntoConstraints = false
nodes().subNodes.forEach { addArrangedSubview($0) }
}
required init(coder: NSCoder) { super.init(coder: coder) }
}
extension HStack: StackModifier {
func alignment(_ alignment: UIStackView.Alignment) -> HStack {
self.alignment = alignment
return self
}
func distribution(_ distribution: UIStackView.Distribution) -> HStack {
self.distribution = distribution
return self
}
func spacing(_ spacing: CGFloat) -> HStack {
self.spacing = spacing
return self
}
}
protocol ViewModifier {
associatedtype View: UIView
func width(_ width: SpacerValue) -> View
func height(_ height: SpacerValue) -> View
func cornerRadius(_ cornerRadius: CGFloat) -> View
func backgroundColor(_ backgroundColor: UIColor) -> View
}
extension UIView: ViewModifier {
@discardableResult
public func width(_ width: SpacerValue) -> UIView {
if let w = constraints.first(where: { $0.firstAnchor == widthAnchor }) {
w.isActive = false
}
translatesAutoresizingMaskIntoConstraints = false
let constraint: NSLayoutConstraint
switch width {
case let .fraction(value, relation):
constraint = NSLayoutConstraint(item: self, attribute: .width, relatedBy: relation, toItem: superview, attribute: .width, multiplier: value, constant: 0)
case let .point(value, relation):
constraint = NSLayoutConstraint(item: self, attribute: .width, relatedBy: relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: value)
}
constraint.priority = UILayoutPriority(999)
constraint.isActive = true
return self
}
@discardableResult
public func height(_ height: SpacerValue) -> UIView {
if let h = constraints.first(where: { $0.firstAnchor == heightAnchor }) {
h.isActive = false
}
let constraint: NSLayoutConstraint
switch height {
case let .fraction(value, relation):
constraint = NSLayoutConstraint(item: self, attribute: .height, relatedBy: relation, toItem: superview, attribute: .height, multiplier: value, constant: 0)
case let .point(value, relation):
constraint = NSLayoutConstraint(item: self, attribute: .height, relatedBy: relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: value)
}
constraint.priority = UILayoutPriority(999)
constraint.isActive = true
return self
}
@discardableResult
public func cornerRadius(_ cornerRadius: CGFloat) -> UIView {
layer.cornerRadius = cornerRadius
return self
}
@discardableResult
public func backgroundColor(_ color: UIColor) -> UIView {
backgroundColor = color
return self
}
@discardableResult
public func contentCompressionResistancePriority(_ priority: UILayoutPriority, axis: NSLayoutConstraint.Axis) -> UIView {
translatesAutoresizingMaskIntoConstraints = false
setContentCompressionResistancePriority(priority, for: axis)
return self
}
@discardableResult
public func contentHuggingPriority(_ priority: UILayoutPriority, axis: NSLayoutConstraint.Axis) -> UIView {
translatesAutoresizingMaskIntoConstraints = false
setContentHuggingPriority(priority, for: axis)
return self
}
}