All Projects → JosephDuffy → Partial

JosephDuffy / Partial

Licence: MIT license
Type-safe wrapper mirroring the wrapped type's properties, making each property optional

Programming Languages

swift
15916 projects

Projects that are alternatives of or similar to Partial

Hero
Elegant transition library for iOS & tvOS
Stars: ✭ 20,547 (+33583.61%)
Mutual labels:  carthage, swiftpm
SwiftUIFormValidator
Declarative form validator for SwiftUI.
Stars: ✭ 34 (-44.26%)
Mutual labels:  carthage, swiftpm
Swiftcrossplatformframework
Tutorial to create cross platform framework for Swift compatible with Carthage and SwiftPM
Stars: ✭ 98 (+60.66%)
Mutual labels:  carthage, swiftpm
Xcglogger
A debug log framework for use in Swift projects. Allows you to log details to the console (and optionally a file), just like you would have with NSLog() or print(), but with additional information, such as the date, function name, filename and line number.
Stars: ✭ 3,710 (+5981.97%)
Mutual labels:  carthage, swiftpm
Imagescout
A Swift implementation of fastimage. Supports PNG, GIF, and JPEG.
Stars: ✭ 940 (+1440.98%)
Mutual labels:  carthage, swiftpm
Dikit
Dependency Injection Framework for Swift, inspired by KOIN.
Stars: ✭ 77 (+26.23%)
Mutual labels:  carthage, swiftpm
Swipycell
Easy to use UITableViewCell implementing swiping to trigger actions.
Stars: ✭ 230 (+277.05%)
Mutual labels:  carthage, swiftpm
SwiftCV
Minimal Swift for TensorFlow OpenCV bindings
Stars: ✭ 37 (-39.34%)
Mutual labels:  swiftpm
DragDropUI
A set of iOS UI components which have drag & drop capability.
Stars: ✭ 30 (-50.82%)
Mutual labels:  carthage
Localize
Localize is a framework writed in swift to localize your projects easier improves i18n, including storyboards and strings.
Stars: ✭ 253 (+314.75%)
Mutual labels:  carthage
Wechatkit
一款快速实现微信第三方登录的框架(Swift版) SDK 1.8.5
Stars: ✭ 249 (+308.2%)
Mutual labels:  carthage
EggSeed
Command Line Tool for Starting Your Swift Packages with Continuous Integration
Stars: ✭ 21 (-65.57%)
Mutual labels:  swiftpm
Validated
A rule-based validation framework
Stars: ✭ 31 (-49.18%)
Mutual labels:  carthage
Colorizeswift
Terminal string styling for Swift.
Stars: ✭ 253 (+314.75%)
Mutual labels:  carthage
OpenSansSwift
Easily use the OpenSans font in Swift
Stars: ✭ 42 (-31.15%)
Mutual labels:  carthage
Simplecheckbox
A simple Checkbox
Stars: ✭ 253 (+314.75%)
Mutual labels:  carthage
AirPlay
Small framework that lets users track iOS AirPlay availability and extra features.
Stars: ✭ 46 (-24.59%)
Mutual labels:  carthage
VGSegment
A segment menu with line animation
Stars: ✭ 19 (-68.85%)
Mutual labels:  carthage
HyperSwift
A Swift DSL for generating HTML and CSS documents
Stars: ✭ 47 (-22.95%)
Mutual labels:  swiftpm
Go-Flashcards
Go Flashcards for iOS and WatchOS - Official repository
Stars: ✭ 60 (-1.64%)
Mutual labels:  carthage

Partial

Build Status Compatible with macOS, iOS, watchOS, tvOS, and Linux Compatible with Swift 5.2+ Supported Xcode Versions SwiftPM Compatible Carthage Compatible CocoaPods Compatible MIT License

Partial is a type-safe wrapper that mirrors the properties of the wrapped type but makes each property optional.

var partialSize = Partial<CGSize>()

partialSize.width = 6016
partialSize.height = 3384
try CGSize(partial: partialSize) // `CGSize(width: 6016, height: 3384)`

partialSize.height = nil
try CGSize(partial: partialSize) // Throws `Partial<CGSize>.Error<CGFloat>.keyPathNotSet(\.height)`

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.

var partialSize = Partial<CGSize>()

// Set key paths
partialSize.width = 6016
partialSize.setValue(3384, for: \.height)

// Retrieve key paths
partialSize.width // `Optional<CGFloat>(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
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:

try fooPartial.value(for: \.bar) // Throws `Partial<Foo>.Error<String?>.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:

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:

protocol PartialConvertible {
    init<PartialType: PartialProtocol>(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:

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:

struct Foo {
    @PartiallyBuilt<CGSize>
    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 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:

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 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.

Remember to add Partial to your Carthage build phase:

$(SRCROOT)/Carthage/Build/iOS/Partial.framework

and

$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Partial.framework

CocoaPods

To install via CocoaPods add the following to your Podfile:

pod 'Partial'

and then run pod install.

License

The project is released under the MIT license. View the LICENSE file for the full license.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].