Partial is fully documented, with generated DocC documentation available online. 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.
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
struct SizeWrapper: PartialConvertible {
let size: CGSize
init<PartialType: PartialProtocol>(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<SizeWrapper>()
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.
let sizeBuilder = PartialBuilder<CGSize>()
let allChangesSubscription = sizeBuilder.subscribeToAllChanges { (keyPath: PartialKeyPath<CGSize>, builder: PartialBuilder<CGSize>) 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:
struct Root {
let size1: CGSize
let size2: CGSize
}
let rootBuilder = PartialBuilder<Root>()
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:
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 Optionals
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:
struct Foo {
let bar: String?
}
var fooPartial = Partial<Foo>()
Setting and retrieving optional values with the setValue(_:for:) and value(for:) functions does not require anything special:
However using dynamic member lookup requires a little more consideration:
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:
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:
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:
extension CGSize: PartialConvertible {
public init<PartialType: PartialProtocol>(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:
let sizeBuilder = PartialBuilder<CGSize>()
// ...
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:
struct Foo {
let size: CGSize
}
var partialFoo = Partial<Foo>()
var partialSize = Partial<CGSize>()
partialSize[\.width] = 6016
try partialFoo.setValue(partialSize, for: \.size) // Throws `Partial<CGSize>.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:
Partial has a full test suite, which is run on GitHub Actions 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. 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 add the package to the dependencies section and as the dependency of a target:
To install via 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. Use the --no-use-binaries flag if this is an issue.
Partial
Partial is a type-safe wrapper that mirrors the properties of the wrapped type but makes each property optional.
Documentation
Partial is fully documented, with generated DocC documentation available online. 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.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:
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 thePartialBuilder
class is provided, which also provides the ability to subscribe to updates.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:
The per-property builders are synchronized using a
Subscription
. You can cancel the subscription by usingPropertyBuilder.detach()
, like so:Dealing with
Optional
sPartials 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:)
andsetValue(_: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:
Setting and retrieving optional values with the
setValue(_:for:)
andvalue(for:)
functions does not require anything special:However using dynamic member lookup requires a little more consideration:
When retrieving values it can be necessary to unwrap the value twice:
Adding support to your own types
Adopting the
PartialConvertible
protocol declares that a type can be initialised with a partial: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 addPartialConvertible
conformance toCGSize
you could usevalue(for:)
to retrieve thewidth
andheight
values:As a convenience it’s then possible to unwrap partials that wrap a type that conforms to
PartialConvertible
: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:
Using the Property Wrapper
PartiallyBuilt
is a property wrapper that can be applied to anyPartialConvertible
property. The property wrapper’sprojectedValue
is aPartialBuilder
, allowing for the following usage:Tests and CI
Partial has a full test suite, which is run on GitHub Actions 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. 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 add the package to the dependencies section and as the dependency of a target:
Carthage
To install via Carthage add to following to your
Cartfile
: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. Use the--no-use-binaries
flag if this is an issue.Remember to add Partial to your Carthage build phase:
and
CocoaPods
To install via CocoaPods add the following to your Podfile:
and then run
pod install
.License
The project is released under the MIT license. View the LICENSE file for the full license.