部分 1.1.0

Partial 1.1.0

Joseph Duffy维护。



Partial 1.1.0

  • Joseph Duffy

Partial

Build Status Documentation Compatible with macOS, iOS, watchOS, tvOS, and Linux SwiftPM Compatible Carthage Compatible CocoaPods Compatible MIT License

Partial 是一个类型安全的包装器,它镜像被包装类型属性的属性,但使每个属性都可选。

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)`

文档

Partial 完全 dokumentiert, mit einem code-level 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

使用概述

Partial有一个基于KeyPath的API,允许它完全类型安全。通过动态成员查找或函数设置、检索和删除键路径是可能的。

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)

键路径注意事项

Swift 中的键路径非常强大,但正因为如此,使用 partial 时存在一些需要注意的地方。

通常我强烈建议您不要将键路径用作属性的属性。原因有两点

  • 在展开部分类型时,会创建歧义。
  • 动态成员搜索不支持属性属性的关键路径。
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

构建复杂类型

由于 Partial 是一个值类型,它不适合在多段代码之间传递。为了允许构建一个类型的单个实例,提供了 PartialBuilder 类,它还提供了订阅更新的能力。

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

在构建更复杂类型时,建议使用每个属性的构建器,并使用这些构建器在根构建器上设置关键路径

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

使用 Subscription 同步每个属性构建器。您可以通过使用 PropertyBuilder.detach() 来取消订阅,如下所示

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

处理 Optional

部分类型正好反映了包装类型的属性,这意味着可选属性仍然将是可选的。在使用动态成员搜索时,这可能不会成为大问题,因为可选值将包裹在另一个可选值中。

下面的例子将使用具有可选属性的类型

struct Foo {
    let bar: String?
}
var fooPartial = Partial<Foo>()

使用 setValue(_:for:)value(for:) 函数设置和获取可选值不需要特殊处理

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`

但是,在动态成员搜索中需要更多的考虑

fooPartial.bar = String?.none // Sets the value to `nil`
fooPartial.bar = nil // Removes the value. Equivalent to setting to `String??.none`

在检索值时,可能需要展开值两次

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")
}

为您自己的类型添加支持

采用 PartialConvertible 协议表示一个类型可以用部分来初始化

protocol PartialConvertible {
    init<PartialType: PartialProtocol>(partial: PartialType) throws where PartialType.Wrapped == Self
}

value(for:) 函数如果没有设置键路径将抛出错误,这在添加一致性时可能很有用。例如,要将 PartialConvertible 一致性添加到 CGSize,您可以使用 value(for:) 来检索 widthheight

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)
    }
}

作为一个便利,您可以展开包裹符合 PartialConvertible 一致性的类型的部分

let sizeBuilder = PartialBuilder<CGSize>()
// ...
let size = try! sizeBuilder.unwrapped()

还可以将键路径设置为部分值。如果解包失败,键路径将不会更新,并且将抛出错误

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)`

使用属性包装器

PartiallyBuilt 是一个可以应用于任何 PartialConvertible 属性的属性包装器。属性包装器的 projectedValue 是一个 PartialBuilder,允许进行以下使用:

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)

测试和持续集成

Partial 拥有完整的测试套件,作为拉取请求的一部分在 Travis CI 上运行。所有测试必须通过才能合并拉取请求。

代码覆盖率收集并报告给 Codecov。不可能达到 100% 的覆盖率;一些代码行应该永远不被触及,但却是为了类型安全所必需的,Swift 不会跟踪作为覆盖率的一部分的 deinit 函数。在审查降低总体代码覆盖率的拉取请求时,将考虑这些限制。

安装

SwiftPM

要通过 SwiftPM 安装,请将包添加到依赖项部分,并作为目标依赖项:

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/JosephDuffy/Partial.git", from: "1.0.0"),
    ],
    targets: [
        .target(name: "MyApp", dependencies: ["Partial"]),
    ],
    ...
)

Carthage

要通过 Carthage 安装,请在您的 Cartfile 中添加以下内容:

github "JosephDuffy/Partial"

运行 carthage update Partial 以构建框架,然后将构建的框架文件拖到您的 Xcode 项目中。Partial 提供预编译的二进制文件,这可能会导致一些符号问题,刷新并拖到 Xcode 项目。如果存在问题,请使用 --no-use-binaries 标志。

请记住将 Partial 添加到您的 Carthage 构建阶段

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

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

CocoaPods

通过 CocoaPods 安装,请将以下内容添加到您的 Podfile 文件中。

pod 'Partial'

然后运行 pod install 命令。

许可证

该项目遵循 MIT 许可。请查看 LICENSE 文件以获取完整许可内容。