Swift package

swift-declarative-configuration

Fluent, composable configuration for Swift values and Cocoa objects

Stored views without initialization ceremony

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
  }()
}
UIKit
import DeclarativeConfiguration
import UIKit

final class SummaryCard: UIView {
  private let contentView = UIView() { $0
    .combined(with: .roundedTop(12))
    .translatesAutoresizingMaskIntoConstraints(false)
    .backgroundColor(.secondarySystemBackground)
  }
}
DeclarativeConfiguration

Here .roundedTop is a project-level reusable configuration, defined below. The call site reads in application order and stays free of temporary names.

From imperative closures to configuration values

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
}()
1 · UIKit

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
}
2 · Then

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)
}
3 · DeclarativeConfiguration

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.

Reuse without polluting the view’s namespace

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,
        ])
      }
    }
  }
}
UIView+Configurations.swift

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.

Escape hatches when assignment is not enough

OperationUse it for
Dynamic-member assignment.text("Settings")
scopeConfigure nested values such as layer
modifyMutate the current property value in place
transformReplace a property using its current value
peekCall 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.