Swift package
Fluent, composable configuration for Swift values and Cocoa objects
UIKit views are often stored properties. Keeping their setup next to the declaration usually means an immediately executed closure, a temporary variable, repeated access paths, and an explicit return. DeclarativeConfiguration turns the object itself into the configuration entry point.
import UIKit
final class SummaryCard: UIView {
private let contentView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .secondarySystemBackground
view.layer.cornerRadius = 12
view.layer.cornerCurve = .continuous
view.layer.maskedCorners = [
.layerMinXMinYCorner,
.layerMaxXMinYCorner,
]
return view
}()
}
import DeclarativeConfiguration
import UIKit
final class SummaryCard: UIView {
private let contentView = UIView() { $0
.combined(with: .roundedTop(12))
.translatesAutoresizingMaskIntoConstraints(false)
.backgroundColor(.secondarySystemBackground)
}
}
Here .roundedTop is a project-level reusable configuration, defined below. The call site reads in application order and stays free of temporary names.
An immediately executed closure keeps setup local, but the statements inside remain imperative and cannot be composed independently of the label.
private let titleLabel: UILabel = {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .headline)
label.textColor = .label
label.numberOfLines = 0
return label
}()
A common first refactor is a generic .with helper. It removes the temporary and return, but each closure still eagerly mutates an already-created object.
private let titleLabel = UILabel().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.font = .preferredFont(forTextStyle: .headline)
$0.textColor = .label
$0.numberOfLines = 0
}
SDC maps writable properties into configuration operations automatically. There is no per-property proxy to declare or maintain.
private let titleLabel = UILabel() { $0
.translatesAutoresizingMaskIntoConstraints(false)
.font(.preferredFont(forTextStyle: .headline))
.textColor(.label)
.numberOfLines(0)
}
Unlike .with, the block builds a lazy Configurator<UILabel> value. That value can be composed, stored, and applied later – not only executed against the object at the call site.
Instead of adding styling methods and properties to UIView, declare reusable setup on the configuration namespace. It appears where configurations are expected without expanding every view instance’s API.
extension Configurator where Base: UIView {
@MainActor
static func roundedTop(_ radius: CGFloat) -> Self {
.init { $0
.layer.scope { $0
.cornerRadius(radius)
.cornerCurve(.continuous)
.maskedCorners([
.layerMinXMinYCorner,
.layerMaxXMinYCorner,
])
}
}
}
}
The configuration can participate in a larger flow, create a configured result, or update an object that already exists.
let card = UIView() { $0
.combined(with: .roundedTop(16))
.backgroundColor(.secondarySystemBackground)
}
let compactCard = UIView().configured(
using: .roundedTop(8)
)
Configurator<UIView>.roundedTop(12)
.configure(existingCard)
Composition follows declaration order, so later steps can deliberately override earlier values. scope keeps related CALayer configuration grouped without repeating .layer for every property.
| Operation | Use it for |
|---|---|
| Dynamic-member assignment | .text("Settings") |
scope | Configure nested values such as layer |
modify | Mutate the current property value in place |
transform | Replace a property using its current value |
peek | Call a method or inspect the configured object |
combined(with:) | Append another reusable configuration |
Use Configurator for inline and reusable pipelines. The instance-bound Builder remains available when a conventional chained builder is clearer. Non-NSObject types can opt in with DefaultConfigurableProtocol.