https://github.com/josephduffy/partial
Type-safe wrapper mirroring the wrapped type's properties, making each property optional
https://github.com/josephduffy/partial
carthage cocoapods property-wrapper swift swiftpm
Last synced: 10 months ago
JSON representation
Type-safe wrapper mirroring the wrapped type's properties, making each property optional
- Host: GitHub
- URL: https://github.com/josephduffy/partial
- Owner: JosephDuffy
- License: mit
- Created: 2019-05-15T22:06:21.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2023-05-05T08:02:32.000Z (about 3 years ago)
- Last Synced: 2025-08-18T19:56:33.991Z (11 months ago)
- Topics: carthage, cocoapods, property-wrapper, swift, swiftpm
- Language: Swift
- Homepage: https://josephduffy.co.uk/tags/partial
- Size: 2.23 MB
- Stars: 79
- Watchers: 2
- Forks: 7
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# Partial
[](https://github.com/JosephDuffy/Partial/actions?query=workflow%3ATests)



[](https://github.com/apple/swift-package-manager)
[](https://github.com/Carthage/Carthage)
[](https://cocoapods.org/pods/Partial)
[](./LICENSE)
Partial is a type-safe wrapper that mirrors the properties of the wrapped type but makes each property optional.
```swift
var partialSize = Partial()
partialSize.width = 6016
partialSize.height = 3384
try CGSize(partial: partialSize) // `CGSize(width: 6016, height: 3384)`
partialSize.height = nil
try CGSize(partial: partialSize) // Throws `Partial.Error.keyPathNotSet(\.height)`
```
# Documentation
Partial 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`.
## Usage overview
Partial 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.
```swift
var partialSize = Partial()
// Set key paths
partialSize.width = 6016
partialSize.setValue(3384, for: \.height)
// Retrieve key paths
partialSize.width // `Optional(6016)`
try partialSize.value(for: \.height) // `3384`
// Remove key paths
partialSize.width = nil
partialSize.removeValue(for: \.width)
```
## Key path considerations
Key paths in Swift are very powerful, but by being so powerful they create a couple of caveats with the usage of partial.
In general **I highly recommend you do not use key paths to a property of a property**. The reason for this is 2 fold:
- It creates ambiguity when unwrapping a partial
- Dynamic member lookup does not support key paths to a property of a property
```swift
struct SizeWrapper: PartialConvertible {
let size: CGSize
init(partial: PartialType) throws where PartialType.Wrapped == SizeWrapper {
// Should unwrap `size` directly...
size = try partial.value(for: \.size)
// ... or unwrap each property of `size`?
let width = try partial.value(for: \.size.width)
let height = try partial.value(for: \.size.height)
size = CGSize(width: width, height: height)
}
}
var sizeWrapperPartial = Partial()
sizeWrapperPartial.size.width = 6016 // This is not possible
```
## Building complex types
Since `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.
```swift
let sizeBuilder = PartialBuilder()
let allChangesSubscription = sizeBuilder.subscribeToAllChanges { (keyPath: PartialKeyPath, builder: PartialBuilder) in
print("\(keyPath) was updated")
}
var widthSubscription = sizeBuilder.subscribeForChanges(to: \.width) { update in
print("width has been updated from \(update.oldValue) to \(update.newValue)")
}
// Notifies both subscribers
partial[\.width] = 6016
// Notifies the all changes subscriber
partial[\.height] = 3384
// Subscriptions can be manually cancelled
allChangesSubscription.cancel()
// Notifies the width subscriber
partial[\.width] = 6016
// Subscriptions will be cancelled when deallocated
widthSubscription = nil
// Does not notify any subscribers
partial[\.width] = 6016
```
When 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:
```swift
struct Root {
let size1: CGSize
let size2: CGSize
}
let rootBuilder = PartialBuilder()
let size1Builder = rootBuilder.builder(for: \.size1)
let size2Builder = rootBuilder.builder(for: \.size2)
size1Builder.setValue(1, for: \.width)
size1Builder.setValue(2, for: \.height)
// These will evaluate to `true`
try? size1Builder.unwrapped() == CGSize(width: 1, height: 2)
try? rootBuilder.value(for: \.size1) == CGSize(width: 1, height: 2)
try? rootBuilder.value(for: \.size2) == nil
```
The per-property builders are synchronized using a `Subscription`. You can cancel the subscription by using `PropertyBuilder.detach()`, like so:
```swift
size2Builder.detach()
size2Builder.setValue(3, for: \.width)
size2Builder.setValue(4, for: \.height)
// These will evaluate to `true`
try? size2Builder.unwrapped() == CGSize(width: 3, height: 4)
try? rootBuilder.value(for: \.size2) == nil
```
## Dealing with `Optional`s
Partials 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.
These examples will use a type that has an optional property:
```swift
struct Foo {
let bar: String?
}
var fooPartial = Partial()
```
Setting and retrieving optional values with the `setValue(_:for:)` and `value(for:)` functions does not require anything special:
```swift
try fooPartial.value(for: \.bar) // Throws `Partial.Error.keyPathNotSet(\.bar)`
fooPartial.setValue(nil, for: \.bar)
try fooPartial.value(for: \.bar) // Returns `String?.none`
```
However using dynamic member lookup requires a little more consideration:
```swift
fooPartial.bar = String?.none // Sets the value to `nil`
fooPartial.bar = nil // Removes the value. Equivalent to setting to `String??.none`
```
When retrieving values it can be necessary to unwrap the value twice:
```swift
if let setValue = fooPartial.bar {
if let unwrapped = setValue {
print("`bar` has been set to", unwrapped)
} else {
print("`bar` has been set to `nil`")
}
} else {
print("`bar` has not been set")
}
```
## Adding support to your own types
Adopting the `PartialConvertible` protocol declares that a type can be initialised with a partial:
```swift
protocol PartialConvertible {
init(partial: PartialType) throws where PartialType.Wrapped == Self
}
```
The `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:
```swift
extension CGSize: PartialConvertible {
public init(partial: PartialType) throws where PartialType.Wrapped == CGSize {
let width = try partial.value(for: \.width)
let height = try partial.value(for: \.height)
self.init(width: width, height: height)
}
}
```
As a convenience it's then possible to unwrap partials that wrap a type that conforms to `PartialConvertible`:
```swift
let sizeBuilder = PartialBuilder()
// ...
let size = try! sizeBuilder.unwrapped()
```
It 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:
```swift
struct Foo {
let size: CGSize
}
var partialFoo = Partial()
var partialSize = Partial()
partialSize[\.width] = 6016
try partialFoo.setValue(partialSize, for: \.size) // Throws `Partial.Error.keyPathNotSet(\.height)`
partialSize[\.height] = 3384
try partialFoo.setValue(partialSize, for: \.size) // Sets `size` to `CGSize(width: 6016, height: 3384)`
```
## Using the Property Wrapper
`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:
```swift
struct Foo {
@PartiallyBuilt
var size: CGSize?
}
var foo = Foo()
foo.size // nil
foo.$size.width = 1024
foo.$size.height = 720
foo.size // CGSize(width: 1024, height: 720)
```
# Tests and CI
Partial 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.
Code 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.
# Installation
## SwiftPM
To install via [SwiftPM](https://github.com/apple/swift-package-manager) add the package to the dependencies section and as the dependency of a target:
```swift
let package = Package(
...
dependencies: [
.package(url: "https://github.com/JosephDuffy/Partial.git", from: "1.0.0"),
],
targets: [
.target(name: "MyApp", dependencies: ["Partial"]),
],
...
)
```
## Carthage
To install via [Carthage](https://github.com/Carthage/Carthage) add to following to your `Cartfile`:
```
github "JosephDuffy/Partial"
```
Run `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.
Remember to [add Partial to your Carthage build phase](https://github.com/Carthage/Carthage#if-youre-building-for-ios-tvos-or-watchos):
```
$(SRCROOT)/Carthage/Build/iOS/Partial.framework
```
and
```
$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Partial.framework
```
## CocoaPods
To install via [CocoaPods](https://cocoapods.org) add the following to your Podfile:
```ruby
pod 'Partial'
```
and then run `pod install`.
# License
The project is released under the MIT license. View the [LICENSE](./LICENSE) file for the full license.