{"id":21064608,"url":"https://github.com/capturecontext/swift-declarative-configuration","last_synced_at":"2026-01-12T09:40:23.441Z","repository":{"id":63906745,"uuid":"297114094","full_name":"CaptureContext/swift-declarative-configuration","owner":"CaptureContext","description":"Declarative configuration for your objects","archived":false,"fork":false,"pushed_at":"2023-12-17T10:57:10.000Z","size":102,"stargazers_count":54,"open_issues_count":3,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-08T11:52:09.135Z","etag":null,"topics":["builder","configuration","declarative","dsl","functional","keypath","spm","swift","swift5-3","swiftpm","ui","uikit"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CaptureContext.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-20T16:12:54.000Z","updated_at":"2025-03-31T19:53:10.000Z","dependencies_parsed_at":"2024-11-19T17:51:19.827Z","dependency_job_id":"69c65ddf-f8dc-463f-93b5-81fa7ff2848b","html_url":"https://github.com/CaptureContext/swift-declarative-configuration","commit_stats":{"total_commits":90,"total_committers":4,"mean_commits":22.5,"dds":"0.24444444444444446","last_synced_commit":"c8b689faa74e6f14c6eee11ac56a6bde0dfa08d5"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CaptureContext%2Fswift-declarative-configuration","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CaptureContext%2Fswift-declarative-configuration/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CaptureContext%2Fswift-declarative-configuration/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CaptureContext%2Fswift-declarative-configuration/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CaptureContext","download_url":"https://codeload.github.com/CaptureContext/swift-declarative-configuration/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254456161,"owners_count":22074119,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["builder","configuration","declarative","dsl","functional","keypath","spm","swift","swift5-3","swiftpm","ui","uikit"],"created_at":"2024-11-19T17:50:16.930Z","updated_at":"2026-01-12T09:40:23.436Z","avatar_url":"https://github.com/CaptureContext.png","language":"Swift","readme":"# swift-declarative-configuration\n\n[![Test](https://github.com/CaptureContext/swift-declarative-configuration/actions/workflows/Test.yml/badge.svg)](https://github.com/CaptureContext/swift-declarative-configuration/actions/workflows/Test.yml) [![SwiftPM 6.2](https://img.shields.io/badge/swiftpm-6.2_|_5.10-ED523F.svg?style=flat)](https://swift.org/download/) ![Platforms](https://img.shields.io/badge/platforms-iOS_11_|_macOS_10.13_|_tvOS_11_|_watchOS_4_|_Catalyst_13-ED523F.svg?style=flat) [![@capture_context](https://img.shields.io/badge/contact-@capture__context-1DA1F2.svg?style=flat\u0026logo=twitter)](https://twitter.com/capture_context) \n\n**DeclarativeConfiguration** provides a declarative, fluent way to configure objects and values in Swift. It enables expressive inline configuration, composable setup logic, and consistent configuration patterns across codebases.\n\n## Table of Contents\n\n- [Motivation](#motivation)\n- [The Problem](#the-problem)\n- [The Solution](#the-solution)\n- [Usage](#usage)\n  - [Inline configuration](#inline-configuration)\n  - [Reusable configuration](#reusable-configuration)\n  - [Composition](#composition)\n  - [Application](#application)\n  - [Scoped configuration](#scoped-configuration)\n  - [Optionals](#optionals)\n  - [Custom types](#custom-types)\n  - [Builder](#builder)\n  - [Known issues](known-issues)\n- [Installation](#installation)\n- [Migration notes](#migration-notes)\n- [License](#license)\n\n## Motivation\n\nConfiguring objects in Swift is usually done imperatively, especially in frameworks like Cocoa:\n\n```swift\nlet label = UILabel()\nlabel.text = \"Hello\"\nlabel.numberOfLines = 0\nlabel.textAlignment = .center\n```\n\nThis style is simple, but it does not scale well. As configuration grows, setup code becomes verbose, repetitive, and hard to keep consistent across a codebase.\n\nTo improve ergonomics, projects often introduce fluent helpers or proxy types. These approaches can make configuration more readable, but they are difficult to generalize and maintain as APIs evolve.\n\nInspired by declarative APIs like SwiftUI, DeclarativeConfiguration improves the ergonomics of object configuration, focusing on expressive and consistent inline setup without per-type helper APIs.\n\n## The Problem\n\nImperative configuration works well for small setups, but it gets noisy as configuration grows, and it is easy to repeat the same patterns across a codebase.\n\n```swift\nlet label = UILabel()\nlabel.text = \"Title\"\nlabel.font = .preferredFont(forTextStyle: .headline)\nlabel.textColor = .secondaryLabel\nlabel.numberOfLines = 0\n```\n\nA common alternative is the “closure initializer” pattern, which keeps setup local, but does not help with composition or reuse.\n\n```swift\nlet label: UILabel = {\n  let label = UILabel()\n  // configuration\n  return label\n}()\n```\n\nAnother popular approach is using small helpers like [Then](https://github.com/devxoul/Then), which improve ergonomics, but still relies on imperative assignments and does not naturally compose configuration beyond the closure.\n```swift\nlet label = UILabel().then {\n  $0.textAlignment = .center\n  $0.textColor = .black\n  $0.text = \"Hello, World!\"\n}\n```\n\nExtracting styles into functions can be a bit more composable, but may pollute types' namespaces and still rely on imperative assignments.\n\n```swift\nextension UILabel {\n  func centeredMultiline() -\u003e Self {\n    self.textAlignment = .center\n    self.numberOfLines = 0\n    return self\n  }\n}\n```\n\nFinally, some projects build fluent proxy types to get a chainable API. This can look great at the call site, but it is difficult to scale because it usually requires per-type and per-property wrappers that must be kept in sync with the underlying framework. Such proxies are also rarely lazy as well as previously mentioned approaches, meaning they depend on an already instantiated object rather than representing configuration as a standalone concept.\n\n```swift\nprotocol _UIViewProtocol: UIView {}\nextension UIView: _UIViewProtocol {}\n\nextension _UIViewProtocol {\n  var proxy: CocoaViewProxy\u003cSelf\u003e { .init(base: self) }\n}\n\nstruct CocoaViewProxy\u003cBase: UIView\u003e {\n  var base: Base\n}\n\nextension CocoaViewProxy where Base: UILabel {\n  func text(_ value: String?) -\u003e Self {\n    base.text = value\n    return self\n  }\n}\n```\n\nWhat’s missing is a generic approach that keeps configuration readable inline, enables composition when it is needed, and avoids maintaining a growing surface area of per-type helper APIs.\n\n## The Solution\n\nDeclarativeConfiguration provides a set of tools to address these problems. The thought process behind its design is fairly straightforward:\n\n- Generic configuration can be represented as a sequence of `(Value) -\u003e Value` transformations\n- Function types in Swift are non-nominal, which means they can't be extended and using plain `(Value) -\u003e Value` would greatly limit API options, that's why we need to wrap it into a wrapper type\n- Wrapper type can provide convenient accessors for all configurable properties by leveraging  [`@dynamicMemberLookup` attribute](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes#dynamicMemberLookup)\n- Wrapper type can help get rid of imperative assignments with [`callAsFunction` method](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/#Methods-with-Special-Names)\n- Wrapper type can provide helpers for scoping values and processing optionals\n\n## Usage\n\n### Inline configuration\n\nThe most common way to use DeclarativeConfiguration is for inline object setup.\n\nInstead of mutating an object step by step, configuration can be expressed as a single, readable block at the call site. Simply call your object as a function with a configuration block.\n\n```swift\nlet label = UILabel() { $0\n  .text(\"Hello\")\n  .textAlignment(.center)\n  .textColor(.secondaryLabel)\n  .numberOfLines(0)\n}\n```\n\nInline configuration works especially well for views and other objects with many configurable properties, where setup code would otherwise become noisy or repetitive.\n\nThere are also a few methods that provide imperative access to the current value:\n\n- Mutable modification: `.intProperty.modify { $0 += 1 }`\n- Immutable transformation: `.intProperty.transform { $0 + 1 }`\n- Peeking: `property.peek { print($0) }`\n\nThe last one is a primary escape path for calling methods when needed:\n\n```swift\n.button.peek { $0.setTitle(\"Title\") }\n```\n\n### Reusable configuration\n\nInline configuration works well for one-offs, but configurations can also be extracted and reused.\n\n#### Declaration\n\nA `Configurator` can be defined as a static value and applied wherever it is needed.\n\n```swift\nextension Configurator where Base: UILabel {\n  @MainActor\n  static var title: Self {\n    .init { $0\n      .font(.preferredFont(forTextStyle: .title))\n      .textColor(.label)\n      .numberOfLines(0)\n      .textAlignment(.center)\n    }\n  }\n}\n```\n\nYou can also simplify declarations by scoping them to the exact type.\n\n\u003e [!NOTE]\n\u003e\n\u003e _Such configurations won't be available to subclasses_\n\n```swift\nextension Configurator\u003cSomeFinalClassView\u003e {\n  @MainActor\n  static var debugGreeting: Self {\n    .init { $0\n       .customTitle(\"Hello, World!\")\n       .backgroundColor(.red)\n    }\n  }\n}\n```\n\n### Composition\n\nConfigurations can be combined to create new ones.\n\n- Using `combined(with:)` method\n- By appending configuration items directly\n\nApplication order matches the declaration order, so you can override values.\n\n```swift\nextension Configurator where Base: UILabel {\n  @MainActor\n  static func blackTitle(alpha: CGFloat = 1) -\u003e Self {\n    .init { $0\n      .combined(with: .title)\n      .textColor(.black.withAlphaComponent(alpha))\n    }\n  }\n\n  @MainActor\n  static func whiteTitle(alpha: CGFloat = 1) -\u003e Self {\n    .title.textColor(.white.withAlphaComponent(alpha))\n  }\n}\n```\n\n### Application\n\nConfigurations can be combined inside an inline block:\n\n```swift\nlet label = UILabel() { $0\n  .combined(with: .title)\n  .text(\"Hello\")\n}\n```\n\nOr applied directly:\n\n```swift\nlet label = UILabel().configured(using: .title.text(\"Hello\"))\n```\n\nDepending on the situation, configurations can also be applied via:\n\n- `config.configure(object)`\n- `config.configured(object)`\n- `config.configure(\u0026value)`\n\n### Scoped configuration\n\nSome properties expose nested objects that require their own configuration. Scoped configuration allows applying configuration to such nested values without breaking the fluent style.\n\nInstead of reaching into nested objects imperatively:\n\n```swift\nlet view = UIView()\nview.layer.cornerRadius = 8\nview.layer.cornerCurve = .continuous\nview.layer.borderWidth = 1\nview.layer.borderColor = UIColor.separator.cgColor\n```\n\nYou can scope configuration to a nested property:\n\n```swift\nlet view = UIView() { $0\n  .layer.scope { $0\n    .cornerRadius(8)\n    .cornerCurve(.continuous)\n    .borderWidth(1)\n    .borderColor(.separator)\n  }\n}\n```\n\nScoped configuration keeps related configuration grouped together and avoids repeating access paths at the call site.\n\nScopes can also be used inside reusable configurations:\n\n```swift\nextension Configurator where Base: UIView {\n  @MainActor\n  static func rounded(\n    radius: CGFloat,\n    curve: UICornerCurve = .continuous\n  ) -\u003e Self {\n    .empty.layer.scope { $0\n      .cornerRadius(radius)\n      .cornerCurve(curve)\n    }   \n  }\n}\n```\n\nScopes compose naturally with other configuration features, including reuse and conditional configuration.\n\n### Optionals\n\nSince the `?` operator in Swift is reserved for optional unwrapping and cannot be overloaded, optional properties in DeclarativeConfiguration are accessed by unwrapping them using the `ifLet` operator.\n\n```swift\n.optionalProperty.ifLet.subproperty(1)\n```\n\nThere is also an equivalent function:\n\n```swift\n.ifLet(\\.optionalProperty).subproperty(1)\n```\n\nSame applies to scoping optional properties\n\n```swift\n.optionalProperty.ifLet.scope { $0 \n  .subproperty1(value1)\n  .subproperty2(value2)\n}\n```\n\n`ifLet` only applies trailing configuration if property value is not `nil`, if you want to specify `defaultValue` you can use `ifLet(else:)`\n\n```swift\n.optionalInt.ifLet(else: 0).modify { $0 += 1 }\n```\n\n#### Conditional application\n\nOptional values can be applied conditionally using `.property(ifLet: value)` API\n\n```swift\nlet subtitle: String? = \"Hello\"\n\nlet label = UILabel() { $0\n  .text(ifLet: subtitle) // applied only if subtitle != nil\n}\n```\n\nThere is also a helper that will register value update only if current value is `nil`\n\n```swift\n.optionalInt.ifNil(0)\n```\n\n### Custom types\n\nAll APIs are already available for `NSObject` subclasses. To enable DeclarativeConfiguration for custom types, conform them to `DefaultConfigurableProtocol`.\n\n```swift\nextension CustomType: DefaultConfigurableProtocol {}\n```\n\n### Builder\n\n`Configurator` is the primary API. Builder is provided for cases where you prefer instance-bound, imperative configuration with chaining.\n\n```swift\nlet label = UILabel().builder\n  .text(\"Hello\")\n  .textAlignment(.center)\n  .textColor(.secondaryLabel)\n  .build()\n```\n\n\u003e `Builder` object can also be instantiated with `Base` value and `Configurator`\n\u003e \n\u003e ```swift\n\u003e Builder(UILabel())\n\u003e Builder(initialValue: { UILabel() }, configuration: initialConfigurator)\n\u003e ```\n\n`.builder` property is available for all NSObject subclasses, custom types must conform to `BuilderProvider` protocol\n\n```swift\nextension CustomType: BuilderProvider {}\n```\n\n`Builder` supports same scoping mechanisms as `Configurator`, and has a few additional methods since it already has access to `Base` value:\n\n- `builder.commit()`  – Applies current configuration to a current `Base` value and returns a new builder with the updated value and empty configuration.\n- `builder.apply()` – Applies current configuration to reference type `Base` without returning the value, useful for silencing _\"Result of call to 'build()' is unused\"_ warning.\n\n### Known Issues\n\n\u003e [!WARNING]\n\u003e\n\u003e The following API won't call configuration block for some reason\n\u003e ```swift\n\u003e struct Example: DefaultConfigurableProtocol {\n\u003e   var property: Int = 0\n\u003e }\n\u003e \n\u003e // Implicit type inference on the rhs of the expression\n\u003e let example: Example = .init() { $0\n\u003e   .property(1)\n\u003e }\n\u003e ```\n\u003e\n\u003e Workarounds:\n\u003e\n\u003e - Use explicit type on the rhs of the expression:\n\u003e\n\u003e   ```swift\n\u003e   let example = Example() { $0\n\u003e     .property(1)\n\u003e   }\n\u003e   ```\n\u003e\n\u003e - Use `.configured` or `.self` or `.callAsFunction` after initializer\n\u003e\n\u003e   ```swift\n\u003e   let example: Example = .init().configured { $0\n\u003e     .property(1)\n\u003e   }\n\u003e   ```\n\u003e\n\u003e Looks like a bug in Swift 🫠\n\n## Installation\n\n### Basic\n\nYou can add DeclarativeConfiguration to an Xcode project by adding it as a package dependency.\n\n1. From the **File** menu, select **Swift Packages › Add Package Dependency…**\n2. Enter [`\"https://github.com/capturecontext/swift-declarative-configuration\"`](https://github.com/capturecontext/swift-declarative-configuration) into the package repository URL text field\n3. Choose products you need to link them to your project.\n\n### Recommended\n\nIf you use SwiftPM for your project structure, add DeclarativeConfiguration to your package file. \n\n```swift\n.package(\n  url: \"git@github.com:capturecontext/swift-declarative-configuration.git\",\n  .upToNextMinor(from: \"0.5.0\")\n)\n```\n\nor via HTTPS\n\n```swift\n.package(\n  url: \"https://github.com:capturecontext/swift-declarative-configuration.git\", \n  .upToNextMinor(from: \"0.5.0\")\n)\n```\n\nDo not forget about target dependencies:\n\n```swift\n.product(\n  name: \"DeclarativeConfiguration\", \n  package: \"swift-declarative-configuration\"\n)\n```\n\n## Migration notes\n\nThe package got major API and package structure changes in `0.4.0`,  here is a list of potential issues when migrating from `0.3.x`\n\n\u003e [!NOTE]\n\u003e\n\u003e _If your migration wasn't intentional you should ensure that you depend on `.upToNextMinor` version as advised in the [installation](#installation) section_\n\n#### Package structure\n\nOld:\n\n- `DeclarativeConfiguration` (umbrella module)\n  - `FunctionalBuilder`\n  - `FunctionalConfigurator`\n  - `FunctionalClosures`\n  - `FunctionalModification`\n\nNew:\n\n- `DeclarativeConfiguration`\n- `DeclarativeConfigurationCore`\n- Deprecated:\n  - `FunctionalBuilder`\n    - exports `DeclarativeConfiguration`\n  - `FunctionalConfigurator`\n    - exports `DeclarativeConfiguration`\n  - `FunctionalModification`\n    - exports `DeclarativeConfiguration`\n  - `FunctionalClosures`\n\n\u003e  Main features of deprecated modules excluding `FunctionalClosures` are now declared right in `DeclarativeConfiguration` module.\n\n\n#### Protocols\n\n- `CustomConfigurable`\n\n- `ConfigInitializable`\n\n- `__ConfigInitializableNSObject`\n\nThese protocols are still available through deprecated [`FunctionalConfigurator` module](Sources/Deprecated/FunctionalConfigurator/Deprecated/Protocols), but this module is [no longer a part of `DeclarativeConfiguration` product](#package-structure) and has to be declared as a separate dependency\n\n#### FunctionalClosures\n\n`Delegates` \u003c `Closures` \u003c `Publishers/Observation/AsyncSequences`\n\nThe module was experimental at the first place and now with a new set of tools in Swift it's probably time to accept that it's not needed anymore, feel free to discuss it in [FunctionalClosures discussion](https://github.com/CaptureContext/swift-declarative-configuration/discussions/7).\n\nIt's [no longer a part of `DeclarativeConfiguration` product](#package-structure), however `FunctionalClosures` product is still available. Consider migrating to modern approaches or simply copying sources. \n\n#### FunctionalKeyPath\n\nPrimary goal for this module was dealing with optional keyPaths, since `writableKeyPath.appending(path: \\.optionalProperty?.subproperty)` is never writable and also it may be tricky to unwrap a keyPath through an optional value. However we found a way to use `subscript`s to achieve this with native `KeyPaths` and extracted our helpers into a separate [swift-keypaths-extensions package](https://github.com/capturecontext/swift-keypaths-extensions).\n\nIt's [no longer a part of `DeclarativeConfiguration` product](#package-structure), however `FunctionalKeyPath` product is still available. Consider migrating to native keyPaths or simply copying sources.\n\n## License\n\nThis library is released under the MIT license. See [LICENSE](./LICENSE) for details.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcapturecontext%2Fswift-declarative-configuration","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcapturecontext%2Fswift-declarative-configuration","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcapturecontext%2Fswift-declarative-configuration/lists"}