Today marks 1 year since I released a blog post demonstrating an implementation of Partial in Swift, and it also marks the release of the 1.0.0 version of a Swift package for Partial.

The package is available on GitHub and supports SwiftPM, Carthage, and CocoaPods.

This blog post will go over some of the changes that have been made since the original blog post, my rationale when making certain decisions, and how I have thought about maintenance. If you want to try out Partial and see how it can be used head over to the GitHub page.

Changes since the original blog post

Since the original blog post on Partial I have used Partial in production, learned a lot more about Swift, and Swift has received a few updates. Throughout this time I have found some shortcomings and have had more time to think about how some aspects should work.

From a consumer's point of view one of the main things that have been updated is that there are no longer any possibilities for ambiguity, which were a real problem with the previous implementation. The PartialBuilder has also been upgraded to provide subscriptions.

Internally the implementation has been simplified, making testing much easier.

Dynamic member lookup

As of writing Swift 5.1 is in beta. As part of the 1.0.0 release I have added support for dynamic member lookup when compiling with Swift 5.1, which is a very nice improvement over the subscripting API:

// Swift 5.0
partial[\.foo]

// Swift 5.1
partial.foo

Version 1.0.0 still supports subscripts, but they are deprecated when using Swift 5.1.

PartialBuilder

In the original blog post I mentioned PartialBuilder, but it was not included in the linked gist. As part of the package release I have included PartialBuilder. The new implementation include subscriptions to all changes and key path changes.

Removal of embedded partials

While at first embedded partials seemed useful I have found that there are better ways to handle setting a partial value on another partial, and it also caused some frustrating ambiguity with the Swift compiler.

Rather than setting a partial value I found it more useful to attempt to set a partial value. For example, assuming CGSize conforms to PartialConvertible, setting a key path of type CGSize to a partial can be done via setValue(_:for:). If the value fails to be unwrapped this function will rethrow the error.

struct SizeWrapper {
    let size: CGSize
}
var wrapperPartial = Partial<SizeWrapper>()
var sizePartial = Partial<CGSize>()
try wrapperPartial.setValue(sizePartial, for: \.size) // Will throw an error, value will not be set
sizePartial.width = 6016
sizePartial.height = 3384
try wrapperPartial.setValue(sizePartial, for: \.size) // Will set `size` to `CGSize(width: 6016, height: 3384)`

This has a few advantages:

  • When unwrapping a PartialConvertible type the implementer does not need to check for partial values
  • It is possible to subscribe for changes to a key path on a PartialBuilder and only be notified when a valid value has been set
  • Internally Partial does need to check for Partial values
  • 2 setValue(_:for:) and 2 partialValue(for:) functions could be removed

I do still find the concept of an embedded partial useful, but propose an alternative approach. For example, you may build an instance across multiple screens, setting the values on a single builder by utilising multiple builders.

struct SizeWrapper {
    let size: CGSize
}
let wrapperBuilder = PartialBuilder<SizeWrapper>()
let sizeSubscription = wrapperBuilder.subscribeForChanges(to: \.size) { update in
    print("Size has been set to \(update.newValue)")
}

let sizeBuilder = PartialBuilder<CGSize>()
let sizeSubscription = wrapperBuilder.subscribeToAllChanges { _, builder in
    do {
        try wrapperBuilder.setValue(builder, for: \.size)
    } catch {
        // Optionally remove the value here, or show the error to the user
        wrapperBuilder.removeValue(for: \.size)
        print("Error unwrapping partial size:", error)
    }
}
sizeBuilder.width = 6016
wrapperBuilder.size // `nil`
sizeBuilder.height = 3384
wrapperBuilder.size // `CGSize(width: 6016, height: 3384)`

Default values can be implemented by providing a custom unwrapping closure:

wrapperBuilder.subscribeToAllChanges { _, builder in
    wrapperBuilder.setValue(builder, for: \.size) { sizePartial in
        let width = sizePartial.width ?? 6016
        let height = sizePartial.height ?? 3384
        return CGSize(width: width, height: height)
    }
}

Note that because the unwrapping closure does not throw the call to setValue(_:for:unwrapper:) does not need to include try.

Removal of Optional-specific functions

When I created the original version of Partial I found it necessary to create separate functions for handling Optionals. I am not sure if a Swift update has made the handling of Optional values better or my understanding around their handling was incorrect, but once I added the test suite and then removed the Optional-specific functions I found them to be unnecessary.

Xcode autocomplete works (sometimes)

When I was writing the original blog post autocomplete for key paths was not provided within Xcode. As of writing this blog post this now works in most situations. For example, typing partial., partial[\.], or partial.value(for: \.) will provide autocomplete, but partial.setValue("new value", for: \.) will not. This is a major improvement, but it's still not perfect.

Future Updates

I wanted to get an MVP version 1.0.0 released as soon as possible. Partly to get the project setup complete so I can utilise the structure in other projects, but also because I have a habit of getting carried away with adding new features and never releasing anything.

One of the features I chose not to include in 1.0.0 is support for Combine. This was partly due to the desire to get a 1.0.0 released, but also because iOS 13 (which is required for Combine) is still in beta. I have used Combine in GatheredKit and can see how simple it would be to implement so it is definitely slated for 1.1.0.

Maintenance

In the past I have found that if I take a break from project a I will often to met with a certain amount of maintenance that needs to be performed before I can start work on the changes I want to make. To combat this I have been making some changes to how I setup my projects to make general maintenance easier:

  • Full test coverage
  • Automatic deployment
  • Automatic dependency updates

Having full test coverage is a no-brainer; I can feel much more confident about changes that are made. This is especially important for smaller projects like Partial because I will likely take larger breaks between chunks of work.

Automatic deployment is setup via git tags; when a tag is pushed Travis CI will create a pre-built binary for Carthage and attach it to a GitHub release. To remove the development dependencies from the SwiftPM package I have setup Rocket, which makes creating a release as easy as running swift run rocket v1.0.0. This vastly reduces the friction required for creating releases, meaning updates won't be sitting unreleased for extended periods of time.

Automatic dependency updates have proved worthwhile for me on other projects, so I have added Dependabot to the project. For my TypeScript projects this is much more useful because all dependencies can be updated, but unfortunately Dependabot does not support any Swift dependency managers (neither do any other dependency managers; I am not throwing shade at Dependabot here). This means that only some dependencies, such as those provided by RubyGems, are automatically updated. Nevertheless this is better than nothing.

Summary

Partial 1.0.0 is a great update compared to the original blog post. It is much easier for me to maintain and is usable in production. If you find any issues, have any questions, and would like to request a feature please open an issue on GitHub or message me on Twitter.


Structs are incredibly useful in Swift, especially when representing static read-only data. However, the values of a struct often come from multiple sources, such as view controllers, network requests, and files on disk, which can make the creation of these structs cumbersome.

There are numerous methods to work around this, but each have their downsides. One of these methods is to change the struct to a class and update the properties to vars, but this removes the advantages of read-only structs. Another is to make a "builder" object, but the API of this object must be kept in-sync with the object is wraps.

Partial eliminates these problems by providing a type-safe API for building structs by utilising generics and KeyPaths. Although I learned of the concept of Partial through TypeScript – which provides Partial as a built-in type – the Swift implementation supports many more use cases.

To demonstrate the use of Partial I will use some simple structs with a few let properties.

struct Order {

    let userId: Int

    let itemIds: [Int]

    let promoCode: String?

    let address: Address

    let billingDetails: BillingDetails

}

struct Address {

    let name: String

    let firstLine: String

    let additionalLines: [String]

    let city: String

    let postCode: String

}

struct BillingDetails {

    let creditCardNumber: String

    let ccv: String

    let address: Address

}

The basic implementation

For simple use cases, only a very simple implementation is required.

struct Partial<Wrapped> {

    private var values: [PartialKeyPath<Wrapped>: Any] = [:]

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? {
        get {
            return values[key] as? ValueType
        }
        set {
            values[key] = newValue
        }
    }

}

You can get and set values by using the subscript of a Partial and passing a KeyPath of the wrapped type.

var partialOrder = Partial<Order>()
partialOrder[\.userId] // nil
partialOrder[\.userId] = 123
partialOrder[\.userId] // 123

However, Partials can be much for more powerful than just this.

Initialisation of the Wrapped Type

One of the first issues you run across with Partials as basic as this is that the initialisation of the wrapped type can be a bit cumbersome.

struct Order {

    // ...

    init?(partial: Partial<Order>) {
        guard let userId = partial[\.userId] else { return nil }
        guard let itemIds = partial[\.itemIds] else { return nil }

        self.userId = userId
        self.itemIds = itemIds
        self.promoCode = partial[\.promoCode]

        // Must check for both scenarios
        let partialAddress = partial[partial: \Order.address]
        if let name = partialAddress[\.name],
            let firstLine = partialAddress[\.firstLine],
            let additionalLines = partialAddress[\.additionalLines],
            let city = partialAddress[\.city],
            let postCode = partialAddress[\.postCode] {
            self.address = Address(name: name, firstLine: firstLine, additionalLines: additionalLines, city: city, postCode: postCode)
        } else if let address = partial[\.address] {
            self.address = address
        } else {
            return nil
        }

        // Same must be done for `billingDetails`...
    }

By defining a new protocol, a throwing function that can retrieve values, and adding a new subscript that can "unwrap" any values stored in sub-Partials, the call site can be much more concise and clear.

protocol PartialConvertible {

    init(partial: Partial<Self>) throws

}

struct Partial<Wrapped> {

    // ...

    enum Error: Swift.Error {
        case missingKey(PartialKeyPath<Wrapped>)
        case invalidValueType(key: PartialKeyPath<Wrapped>, actualValue: Any)
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            return Error.missingKey(key)
        }
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? where ValueType: PartialConvertible {
        get {
            return try? value(for: key)
        }
        set {
            values[key] = newValue
        }
    }

}

struct ExportOptions: PartialConvertible {

    // ...

   init(partial: Partial<Order>) throws {
        userId = try partial.value(for: \.userId)
        itemIds = try partial.value(for: \.itemIds)
        promoCode = try partial.value(for: \.promoCode)
        address = try partial.value(for: \.address)
        billingDetails = try partial.value(for: \.billingDetails)
    }

}

extension Order.Address: PartialConvertible {

    init(partial: Partial<Order.Address>) throws {
        name = try partial.value(for: \.name)
        firstLine = try partial.value(for: \.firstLine)
        additionalLines = try partial.value(for: \.additionalLines)
        city = try partial.value(for: \.city)
        postCode = try partial.value(for: \.postCode)
    }

}

extension Order.BillingDetails: PartialConvertible {

    init(partial: Partial<Order.BillingDetails>) throws {
        creditCardNumber = try partial.value(for: \.creditCardNumber)
        ccv = try partial.value(for: \.ccv)
        address = try partial.value(for: \.address)
    }

}

Much better!

Recursive Partials

Partials on their own are great, but once you try to access a property of a property of a Partial it stops working quite as expected.

partialOrder[\.address][\.name] = "Santa Claus" // Not possible

To support this a new subscript and a value(for:) function that utilises the PartialConvertible protocol is required.


struct Partial<Wrapped> {

    // ...

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        guard let value = values[key] else {
            throw Error.missingKey(key)
        }
        if let value = value as? ValueType {
            return value
        } else if let partial = value as? Partial<ValueType> {
           return try ValueType(partial: partial)
        } else {
           throw Error.invalidValueType(key: key, actualValue: value)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            return values[key] as? Partial<ValueType> ?? Partial<ValueType>()
        }
        set {
            values[key] = newValue
        }
    }

}

partialOrder[\.address][\.name] // nil
partialOrder[\.address][\.name] = "Johnny Appleseed"
partialOrder[\.address][\.name] // "Johnny Appleseed"

However, because it will always return a Partial, there will be an issue if the value has been explictly set elsewhere:

partialOrder[\.address] = Address(name: "Johnny Appleseed", ...)
partialOrder[\.address] // An empty `Partial`
try? Order.Address(partial: partialOrder[\.address]) // nil

To support this a backing value is added, allowing the stored value to be wrapped and its properties overridden.

The type of the values property is also updated to [PartialKeyPath<Wrapped>: Any?] and subscript setters are updated to use the updateValue(_:forKey:) function. This is to support unsetting values by assigning a key to nil when a backing value is used.

struct Partial<Wrapped> {

    // ...

    private var values: [PartialKeyPath<Wrapped>: Any?] = [:]

    private var backingValue: Wrapped? = nil

    init(backingValue: Wrapped? = nil) {
        self.backingValue = backingValue
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? {
        get {
            return try? value(for: key)
        }
        set {
            values.updateValue(newValue, forKey: key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            if let value = try? self.value(for: key) {
                return Partial<ValueType>(backingValue: value)
            } else if let partial = values[key] as? Partial<ValueType> {
                return partial
            } else {
                return Partial<ValueType>()
            }
        }
        set {
            values.updateValue(newValue, forKey: key)
        }
    }

}

partialOrder[\.address] = Address(name: "Mr Appleseed", ...)
partialOrder[\.address][\.name] // "Mr Appleseed"
partialOrder[\.billingAddress][\.address] = partialOrder[\.address]
partialOrder[\.billingAddress][\.address][\.name] = "Johnny Appleseed"
partialOrder[\.billingAddress][\.address][\.name] // "Johnny Appleseed"

Dealing with Optional Properties

When using a property of Wrapped that's optional, such as promoCode on Order, the type of partial[\.promoCode] will be String??. To work around this every function and subscript needs to be duplicated to support a key of type KeyPath<Wrapped, ValueType?>.

For the sake of brevity, only one of these is shown below.

struct Partial<Wrapped> {

    // ...

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType?>) -> ValueType? {
        get {
            return try? value(for: key)
        }
        set {
            values.updateValue(newValue, forKey: key)
        }
    }

}

Swift will pick the right one based on context.

Downsides

Using Partial does have some downsides. One is that you still have to create a custom init function, a requirement that could be removed by adding Partial to the standard library or made easier using metaprogramming tools such as Sourcery.

Another downside is that Xcode will not provide autocomplete suggestions for KeyPaths, unless the type is provided before the period.

partialOrder[\.userId] // Will not autocomplete
partialOrder[\Order.userId] // Will autocomplete

This is not an issue with Partial itself but is a shortcoming due to its reliance on KeyPath

Partial is still a value type, which prevents the same instance being passed between objects. Some may choose to update Partial to be a class, but I prefer to provide a small wrapper in the form of a class with a single partial property and a convenient function for PartialConvertible values. It could be subclassed or extended to support per-type convenience functions, a delegate, a completion closure, etc.

class PartialBuilder<Wrapped> {

    var partial: Partial<Wrapped>

    init(partial: Partial<Wrapped> = Partial<Wrapped>()) {
        self.partial = partial
    }

    init(backingValue: Wrapped) {
        partial = Partial(backingValue: backingValue)
    }

}

extension PartialBuilder where Wrapped: PartialConvertible {

    func unwrappedValue() throws -> Wrapped {
        return try Wrapped(partial: partial)
    }

}

Full Code

If you want to try Partial yourself you can download the playground, or download Partial.swift and add it to your project.

Below is the full code – excluding documentation and CustomStringConvertible and CustomDebugStringConvertible conformance for the sake of brevity – plus a full example of how Partial can be used.

struct Partial<Wrapped>: CustomStringConvertible, CustomDebugStringConvertible {

    enum Error<ValueType>: Swift.Error {
        case missingKey(KeyPath<Wrapped, ValueType>)
        case invalidValueType(key: KeyPath<Wrapped, ValueType>, actualValue: Any)
    }

    private var values: [PartialKeyPath<Wrapped>: Any?] = [:]

    private var backingValue: Wrapped? = nil}
    }

    init(backingValue: Wrapped? = nil) {
        self.backingValue = backingValue
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let value = value {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        }

        throw Error.missingKey(key)
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType?>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let value = value {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        }

        throw Error.missingKey(key)
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else if let value = value {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        }

        throw Error.missingKey(key)
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType?>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else if let value = value {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        }

        throw Error.missingKey(key)
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? {
        get {
            return try? value(for: key)
        }
        set {
            values.updateValue(newValue, forKey: key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType?>) -> ValueType? {
        get {
            return try? value(for: key)
        }
        set {
            values.updateValue(newValue, forKey: key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            if let value = try? self.value(for: key) {
                return Partial<ValueType>(backingValue: value)
            } else if let partial = values[key] as? Partial<ValueType> {
                return partial
            } else {
                return Partial<ValueType>()
            }
        }
        set {
            values.updateValue(newValue, forKey: key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType?>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            if let value = try? self.value(for: key) {
                return Partial<ValueType>(backingValue: value)
            } else if let partial = values[key] as? Partial<ValueType> {
                return partial
            } else {
                return Partial<ValueType>()
            }
        }
        set {
            values.updateValue(newValue, forKey: key)
        }
    }

}

var partialOrder = Partial<Order>()
partialOrder[\.userId] = 123
partialOrder[\.itemIds] = [1, 4, 7]
partialOrder[\.promoCode] = "HELLO10"
partialOrder[\.address] = Address(name: "Johnny Appleseed", firstLine: "One Infinite Loop", additionalLines: ["Cupertino"], city: "CA", postCode: "95014")
partialOrder[\.billingDetails][\.creditCardNumber] = "1111 2222 3333 4444"
partialOrder[\.billingDetails][\.ccv] = "123"
partialOrder[\.billingDetails][\.address] = partialOrder[\.address]
partialOrder[\.billingDetails][\.address][\.name] = "Santa Claus"
partialOrder[\.billingDetails][\.address][\.firstLine] = "Santa's Grotto"
partialOrder[\.billingDetails][\.address][\.additionalLines] = []
partialOrder[\.billingDetails][\.address][\.city] = "Reindeerland"
partialOrder[\.billingDetails][\.address][\.postCode] = "XM4 5HQ"

do {
    let order = try Order(partial: partialOrder)
    order.userId // 123
    order.address.name // Johnny Appleseed
    order.billingDetails.address.name // "Santa Clause"
} catch {
    error
}

Special Thanks

Shaps helped me a lot with this post, from working with me through the evolution of the implementation to reading drafts of this post. Thanks, Shaps!


Gathered 1.3 has been released and is now available on the App Store. Version 1.3 brings 2 new data sources, app-wide speed and UX improvements, and support for various features added in recent versions of iOS.

This update also has lots of behind-the-scenes changes that will make future updates easier to create and deploy, which – along with my features roadmap – should mean more frequent updates.

I wasn't very happy removing the Heart Rate data source but Apple weren't very happy with the use of HealthKit.

Full Changelog

  • Adds Advertising and Authentication data sources
  • Removes the Heart Rate (via Apple Watch) data source at the request of Apple
  • Data sources can now be reordered
  • Values can now be copied by tapping the cell
  • Adds support for iPhone X
  • Improves layout on iPads
  • Adds drag and drop support for recordings on iPads running iOS 11 or newer
  • Altimeter's "Relative Altitude" value can be reset to zero by tapping the cell
  • Adds "Speed (estimated)" to GPS data source
  • A "Start Recording" Quick Action has been added to the home screen icon
  • Recordings will now always use the update frequency set in the Settings tab
  • Fixes some exported CSV files being invalid
  • Fixes the Microphone data source pausing other audio
  • Fixes a crash that may occur when stopping a recording