{"id":17160566,"url":"https://github.com/josephduffy/partial","last_synced_at":"2025-08-28T04:47:32.829Z","repository":{"id":37588116,"uuid":"186909113","full_name":"JosephDuffy/Partial","owner":"JosephDuffy","description":"Type-safe wrapper mirroring the wrapped type's properties, making each property optional","archived":false,"fork":false,"pushed_at":"2023-05-05T08:02:32.000Z","size":2334,"stargazers_count":79,"open_issues_count":10,"forks_count":7,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-18T19:56:33.991Z","etag":null,"topics":["carthage","cocoapods","property-wrapper","swift","swiftpm"],"latest_commit_sha":null,"homepage":"https://josephduffy.co.uk/tags/partial","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/JosephDuffy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","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},"funding":{"github":"josephduffy"}},"created_at":"2019-05-15T22:06:21.000Z","updated_at":"2025-08-04T08:00:16.000Z","dependencies_parsed_at":"2024-10-14T22:25:19.002Z","dependency_job_id":"fe7a5dd9-55a8-4cc6-b9dd-e24b7d5a11f6","html_url":"https://github.com/JosephDuffy/Partial","commit_stats":{"total_commits":558,"total_committers":6,"mean_commits":93.0,"dds":0.2992831541218638,"last_synced_commit":"65388642c25a206afe92741c5cda3b65c1c17ae5"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/JosephDuffy/Partial","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPartial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPartial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPartial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPartial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JosephDuffy","download_url":"https://codeload.github.com/JosephDuffy/Partial/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPartial/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272440271,"owners_count":24935425,"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","status":"online","status_checked_at":"2025-08-28T02:00:10.768Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["carthage","cocoapods","property-wrapper","swift","swiftpm"],"created_at":"2024-10-14T22:25:16.157Z","updated_at":"2025-08-28T04:47:32.798Z","avatar_url":"https://github.com/JosephDuffy.png","language":"Swift","funding_links":["https://github.com/sponsors/josephduffy"],"categories":[],"sub_categories":[],"readme":"# Partial\n\n[![Build Status](https://github.com/JosephDuffy/Partial/workflows/Tests/badge.svg)](https://github.com/JosephDuffy/Partial/actions?query=workflow%3ATests)\n![Compatible with macOS, iOS, watchOS, tvOS, and Linux](https://img.shields.io/badge/platforms-macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Linux-4BC51D.svg)\n![Compatible with Swift 5.2+](https://img.shields.io/badge/swift-5.2%2B-4BC51D.svg)\n![Supported Xcode Versions](https://img.shields.io/badge/Xcode-12.5.1%20%7C%2013.2.1-success)\u003c!---xcode-version-badge-markdown--\u003e\n[![SwiftPM Compatible](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift-package-manager)\n[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n[![CocoaPods Compatible](https://img.shields.io/badge/CocoaPods-compatible-4BC51D.svg?style=flat)](https://cocoapods.org/pods/Partial)\n[![MIT License](https://img.shields.io/badge/License-MIT-4BC51D.svg?style=flat)](./LICENSE)\n\nPartial is a type-safe wrapper that mirrors the properties of the wrapped type but makes each property optional.\n\n```swift\nvar partialSize = Partial\u003cCGSize\u003e()\n\npartialSize.width = 6016\npartialSize.height = 3384\ntry CGSize(partial: partialSize) // `CGSize(width: 6016, height: 3384)`\n\npartialSize.height = nil\ntry CGSize(partial: partialSize) // Throws `Partial\u003cCGSize\u003e.Error\u003cCGFloat\u003e.keyPathNotSet(\\.height)`\n```\n\n# Documentation\n\nPartial is fully documented, with [generated DocC documentation available online](https://josephduffy.github.io/Partial/documentation/partial/). The online documentation is generated from the source code with every release, so it is up-to-date with the latest release, but may be different to the code in `master`.\n\n## Usage overview\n\nPartial has a `KeyPath`-based API, allowing it to be fully type-safe. Setting, retrieving, and removing key paths is possible via dynamic member lookup or functions.\n\n```swift\nvar partialSize = Partial\u003cCGSize\u003e()\n\n// Set key paths\npartialSize.width = 6016\npartialSize.setValue(3384, for: \\.height)\n\n// Retrieve key paths\npartialSize.width // `Optional\u003cCGFloat\u003e(6016)`\ntry partialSize.value(for: \\.height) // `3384`\n\n// Remove key paths\npartialSize.width = nil\npartialSize.removeValue(for: \\.width)\n```\n\n## Key path considerations\n\nKey paths in Swift are very powerful, but by being so powerful they create a couple of caveats with the usage of partial.\n\nIn general **I highly recommend you do not use key paths to a property of a property**. The reason for this is 2 fold:\n\n- It creates ambiguity when unwrapping a partial\n- Dynamic member lookup does not support key paths to a property of a property\n\n```swift\nstruct SizeWrapper: PartialConvertible {\n    let size: CGSize\n\n    init\u003cPartialType: PartialProtocol\u003e(partial: PartialType) throws where PartialType.Wrapped == SizeWrapper {\n        // Should unwrap `size` directly...\n        size = try partial.value(for: \\.size)\n        // ... or unwrap each property of `size`?\n        let width = try partial.value(for: \\.size.width)\n        let height = try partial.value(for: \\.size.height)\n        size = CGSize(width: width, height: height)\n    }\n}\n\nvar sizeWrapperPartial = Partial\u003cSizeWrapper\u003e()\nsizeWrapperPartial.size.width = 6016 // This is not possible\n```\n\n## Building complex types\n\nSince `Partial` is a value type it is not suitable for being passed between multiple pieces of code. To allow for a single instance of a type to be constructed the `PartialBuilder` class is provided, which also provides the ability to subscribe to updates.\n\n```swift\nlet sizeBuilder = PartialBuilder\u003cCGSize\u003e()\nlet allChangesSubscription = sizeBuilder.subscribeToAllChanges { (keyPath: PartialKeyPath\u003cCGSize\u003e, builder: PartialBuilder\u003cCGSize\u003e) in\n    print(\"\\(keyPath) was updated\")\n}\nvar widthSubscription = sizeBuilder.subscribeForChanges(to: \\.width) { update in\n    print(\"width has been updated from \\(update.oldValue) to \\(update.newValue)\")\n}\n\n// Notifies both subscribers\npartial[\\.width] = 6016\n\n// Notifies the all changes subscriber\npartial[\\.height] = 3384\n\n// Subscriptions can be manually cancelled\nallChangesSubscription.cancel()\n// Notifies the width subscriber\npartial[\\.width] = 6016\n\n// Subscriptions will be cancelled when deallocated\nwidthSubscription = nil\n// Does not notify any subscribers\npartial[\\.width] = 6016\n```\n\nWhen building a more complex type I recommend using a builder per-property and using the builders to set the key paths on the root builder:\n\n```swift\nstruct Root {\n    let size1: CGSize\n    let size2: CGSize\n}\n\nlet rootBuilder = PartialBuilder\u003cRoot\u003e()\nlet size1Builder = rootBuilder.builder(for: \\.size1)\nlet size2Builder = rootBuilder.builder(for: \\.size2)\n\nsize1Builder.setValue(1, for: \\.width)\nsize1Builder.setValue(2, for: \\.height)\n\n// These will evaluate to `true`\ntry? size1Builder.unwrapped() == CGSize(width: 1, height: 2)\ntry? rootBuilder.value(for: \\.size1) == CGSize(width: 1, height: 2)\ntry? rootBuilder.value(for: \\.size2) == nil\n```\n\nThe per-property builders are synchronized using a `Subscription`. You can cancel the subscription by using `PropertyBuilder.detach()`, like so:\n\n```swift\nsize2Builder.detach()\nsize2Builder.setValue(3, for: \\.width)\nsize2Builder.setValue(4, for: \\.height)\n\n// These will evaluate to `true`\ntry? size2Builder.unwrapped() == CGSize(width: 3, height: 4)\ntry? rootBuilder.value(for: \\.size2) == nil\n```\n\n## Dealing with `Optional`s\n\nPartials mirror the properties of the wrapping type exactly, meaning that optional properties will still be optional. This isn't much of a problem with the `value(for:)` and `setValue(_:for:)` functions, but can be a bit more cumbersome when using dynamic member lookup because the optional will be wrapped in another optional.\n\nThese examples will use a type that has an optional property:\n\n```swift\nstruct Foo {\n    let bar: String?\n}\nvar fooPartial = Partial\u003cFoo\u003e()\n```\n\nSetting and retrieving optional values with the `setValue(_:for:)` and `value(for:)` functions does not require anything special:\n\n```swift\ntry fooPartial.value(for: \\.bar) // Throws `Partial\u003cFoo\u003e.Error\u003cString?\u003e.keyPathNotSet(\\.bar)`\nfooPartial.setValue(nil, for: \\.bar)\ntry fooPartial.value(for: \\.bar) // Returns `String?.none`\n```\n\nHowever using dynamic member lookup requires a little more consideration:\n\n```swift\nfooPartial.bar = String?.none // Sets the value to `nil`\nfooPartial.bar = nil // Removes the value. Equivalent to setting to `String??.none`\n```\n\nWhen retrieving values it can be necessary to unwrap the value twice:\n\n```swift\nif let setValue = fooPartial.bar {\n    if let unwrapped = setValue {\n        print(\"`bar` has been set to\", unwrapped)\n    } else {\n        print(\"`bar` has been set to `nil`\")\n    }\n} else {\n    print(\"`bar` has not been set\")\n}\n```\n\n## Adding support to your own types\n\nAdopting the `PartialConvertible` protocol declares that a type can be initialised with a partial:\n\n```swift\nprotocol PartialConvertible {\n    init\u003cPartialType: PartialProtocol\u003e(partial: PartialType) throws where PartialType.Wrapped == Self\n}\n```\n\nThe `value(for:)` function will throw an error if the key path has not been set, which can be useful when adding conformance. For example, to add `PartialConvertible` conformance to `CGSize` you could use `value(for:)` to retrieve the `width` and `height` values:\n\n```swift\nextension CGSize: PartialConvertible {\n    public init\u003cPartialType: PartialProtocol\u003e(partial: PartialType) throws where PartialType.Wrapped == CGSize {\n        let width = try partial.value(for: \\.width)\n        let height = try partial.value(for: \\.height)\n        self.init(width: width, height: height)\n    }\n}\n```\n\nAs a convenience it's then possible to unwrap partials that wrap a type that conforms to `PartialConvertible`:\n\n```swift\nlet sizeBuilder = PartialBuilder\u003cCGSize\u003e()\n// ...\nlet size = try! sizeBuilder.unwrapped()\n```\n\nIt is also possible to set a key path to a partial value. If the unwrapping fails the key path will not be updated and the error will be thrown:\n\n```swift\nstruct Foo {\n    let size: CGSize\n}\n\nvar partialFoo = Partial\u003cFoo\u003e()\nvar partialSize = Partial\u003cCGSize\u003e()\n\npartialSize[\\.width] = 6016\ntry partialFoo.setValue(partialSize, for: \\.size) // Throws `Partial\u003cCGSize\u003e.Error.keyPathNotSet(\\.height)`\n\npartialSize[\\.height] = 3384\ntry partialFoo.setValue(partialSize, for: \\.size) // Sets `size` to `CGSize(width: 6016, height: 3384)`\n```\n\n## Using the Property Wrapper\n\n`PartiallyBuilt` is a property wrapper that can be applied to any `PartialConvertible` property. The property wrapper's `projectedValue` is a `PartialBuilder`, allowing for the following usage:\n\n```swift\nstruct Foo {\n    @PartiallyBuilt\u003cCGSize\u003e\n    var size: CGSize?\n}\n\nvar foo = Foo()\nfoo.size // nil\nfoo.$size.width = 1024\nfoo.$size.height = 720\nfoo.size // CGSize(width: 1024, height: 720)\n```\n\n# Tests and CI\n\nPartial has a full test suite, which is run on [GitHub Actions](https://github.com/JosephDuffy/Partial/actions?query=workflow%3ATests) as part of pull requests. All tests must pass for a pull request to be merged.\n\nCode coverage is collected and reported to to [Codecov](https://codecov.io/gh/JosephDuffy/Partial). 100% coverage is not possible; some lines of code should never be hit but are required for type-safety, and Swift does not track `deinit` functions as part of coverage. These limitations will be considered when reviewing a pull request that lowers the overall code coverage.\n\n# Installation\n\n## SwiftPM\n\nTo install via [SwiftPM](https://github.com/apple/swift-package-manager) add the package to the dependencies section and as the dependency of a target:\n\n```swift\nlet package = Package(\n    ...\n    dependencies: [\n        .package(url: \"https://github.com/JosephDuffy/Partial.git\", from: \"1.0.0\"),\n    ],\n    targets: [\n        .target(name: \"MyApp\", dependencies: [\"Partial\"]),\n    ],\n    ...\n)\n```\n\n## Carthage\n\nTo install via [Carthage](https://github.com/Carthage/Carthage) add to following to your `Cartfile`:\n\n```\ngithub \"JosephDuffy/Partial\"\n```\n\nRun `carthage update Partial` to build the framework and then drag the built framework file in to your Xcode project. Partial provides pre-compiled binaries, [which can cause some issues with symbols](https://github.com/Carthage/Carthage#dwarfs-symbol-problem). Use the `--no-use-binaries` flag if this is an issue.\n\nRemember to [add Partial to your Carthage build phase](https://github.com/Carthage/Carthage#if-youre-building-for-ios-tvos-or-watchos):\n\n```\n$(SRCROOT)/Carthage/Build/iOS/Partial.framework\n```\n\nand\n\n```\n$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Partial.framework\n```\n\n## CocoaPods\n\nTo install via [CocoaPods](https://cocoapods.org) add the following to your Podfile:\n\n```ruby\npod 'Partial'\n```\n\nand then run `pod install`.\n\n# License\n\nThe project is released under the MIT license. View the [LICENSE](./LICENSE) file for the full license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosephduffy%2Fpartial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjosephduffy%2Fpartial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosephduffy%2Fpartial/lists"}