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

Sharing a location on iOS is something that not a lot of apps need, but after requiring it for my latest app, Scanula, I found that there isn't a good resource explaining how to do it properly. This is the first post in a series of planned posts going over a few of the tips, tricks, and common pitfalls I have found while working with iOS Share Sheets.

Sharing on iOS - An Overview

Standard iOS "Action" Icon

Sharing on iOS is done using the Share Sheet, which is often opened via the "Action" icon (shown left). When tapping this, the user is presented with a Share Sheet, which provides various options, depending on the item being shared. In the blog post we'll be looking at location exclusively, but there are a various things that can be shared, from images, to URLs, to text files. The full list can be found in Apple's Documention.

UIActivityViewController and UIActivityItem

There's not a whole lot of documentation that helps determine exactly how to use UIActivityViewController and its associated classes, but the simplest way to use it would be:

// This code assumes:
// - This is inside a subclass of `UIViewController`
// - This is inside a class that contains a property called `shareBarButtonItem` of type `UIBarButtonItem`

let activityItems: [AnyObject] = [
    "A shared piece of text"
];

let vc = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)

// If run on iPad, this is required
vc.popoverPresentationController?.barButtonItem = shareBarButtonItem

presentViewController(vc, animated: true, completion: nil)

This would present a Share Sheet sharing "A shared piece of text". This would allow the user to share this text via various built-in applications, such as Messages, Mail, or Notes, via 3rd party apps, such as Dropbox or Facebook, or via AirDrop. On its own, this isn't particularly useful, but it's a start.

Note that UIActivityViewController's designated initialiser takes in an array of AnyObject. This may look like an open invitation to just pop anything in the array, but it's actually far from that. Even though it's smart enough to figure out what you want with simple things (such as text, as shown in the example), it cant infer what you want to send for all items.

For more complex items, items should conform to UIActivityItemSource (something I hope to write another blog post on). However, in this case, simply using NSItemProvider will help a lot.

Sharing Locations

The primary focus for this blog post is going to be sharing locations. So, without further adu, here's the meat to go with your potatoes.

The "Copy/Paste From StackOverflow" way

When searching Google for "uiactivityviewcontroller share location", the top results (I've not checked all ~150,000) point to a very similar solution:

  • Create a VCard containing the location
    • Note that some solutions suggested creating an contact via the AddressBook framework and using that to generate the VCard contents, crazy!
  • Write the VCard data to a temperary location on disk
  • Pass in the NSURL of the file

Here's my example code:

// Note: Don't use this code!
func activityItems(latitude: Double, longitude: Double) -> [AnyObject]? {
    var items = [AnyObject]()

    let locationTitle = "Shared Location"

    let locationVCardString = [
        "BEGIN:VCARD",
        "VERSION:3.0",
        "PRODID:-//Joseph Duffy//Blog Post Example//EN",
        "N:;\(locationTitle);;;",
        "FN:\(locationTitle)",
        "item1.URL;type=pref:https://maps.apple.com/?ll=\(latitude),\(longitude)",
        "item1.X-ABLabel:map url",
        "END:VCARD"
        ].joinWithSeparator("\n")

    guard let vCardData = locationVCardString.dataUsingEncoding(NSUTF8StringEncoding) else {
        return nil
    }

    let fileManager = NSFileManager.defaultManager()
    guard let cacheDirectory = try? fileManager.URLForDirectory(.CachesDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true) else {
        return nil
    }

    let fileLocation = cacheDirectory.URLByAppendingPathComponent("\(latitude),\(longitude).loc.vcf")
    vCardData.writeToURL(fileLocation, atomically: true)

    return [
        fileLocation
    ]
}

While this does technically work for most use cases, when sharing via AirDrop the items is interpreted as a file (as it techncially should). This has some unwanted side effects:

  • Some apps that should be able to share a location (such as Facebook's Messenger) see the item as a file and refuse to share it
  • When sharing via AirDrop, the item is shared as a contact card, and the user is prompted to add the contact the their contacts, not view the location
  • The information is written to disk, which while not being a big deal, can be avoided, so why not just keep it in memory?

Lead By Example - The Apple way

When trying to figure out the correct way of doing this I created a smalled app for debugging Share Sheet items (hopefully more on this in another blog post). This shows me that Apple's built-in Maps application does things a little differently by sharing:

  • A single text item (the title of the location)
  • An Apple Maps NSURL
  • The location in the form of a VCard, but simply as NSData (not stored in a file)

Doing this is fairly easy. Here's my code to do it:

func activityItems(latitude: Double, longitude: Double) -> [AnyObject]? {
    var items = [AnyObject]()

    let locationTitle = "Shared Location"
    let URLString = "https://maps.apple.com?ll=\(latitude),\(longitude)"

    if let url = NSURL(string: URLString) {
        items.append(url)
    }

    let locationVCardString = [
        "BEGIN:VCARD",
        "VERSION:3.0",
        "PRODID:-//Joseph Duffy//Blog Post Example//EN",
        "N:;\(locationTitle);;;",
        "FN:\(locationTitle)",
        "item1.URL;type=pref:\(URLString)",
        "item1.X-ABLabel:map url",
        "END:VCARD"
        ].joinWithSeparator("\n")

    guard let vCardData = locationVCardString.dataUsingEncoding(NSUTF8StringEncoding) else {
        return nil
    }

    let vCardActivity = NSItemProvider(item: vCardData, typeIdentifier: kUTTypeVCard as String)

    items.append(vCardActivity)

    items.append(locationTitle)

    return items
}

This doesn't require much more code, but has a few other added bonuses:

  • When shared via AriDrop, ipens Maps.app
  • Allows sharing via apps that don't support sharing file, such as Facebook's Messenger
  • Allows the user to copy the location to the clipboard in form of "<url> <share title>"

I've been doing a lot of work with Share Sheets lately, so if you've found this post useful and want to see more, check back soon, subscribe to the RSS feed for this blog, or follow me on Twitter.