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 是一个类型安全的包装器,它镜像了被包装类型的属性,但使每个属性都变为可选的。

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 有完整的文档,在线提供了生成的 DocC 文档。在线文档是从每次发布的源代码生成的,因此它是最新的,但可能与 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(可选类型)

Partials 准确地镜像了包装类型的属性,这意味着可选属性仍然是可选的。这对于 value(for:)setValue(_:for:) 函数来说没什么大问题,但在使用动态成员查找时可能会有些麻烦,因为可选类型会被包装在另一个可选类型中。

以下示例将使用具有可选属性的类型。

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 协议声明一个类型可以用 partial 初始化。

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 类型的 partial。

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

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

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)

测试和 CI

Partial 有一个完整的测试套件,该套件在 GitHub Actions 上作为 pull request 的一部分运行。所有测试必须通过才能合并 pull request。

代码覆盖率被收集并报告给 Codecov。100% 的覆盖率是不可能的;某些代码行永远不应该被命中,但类型安全是必需的,并且 Swift 不会将 deinit 函数作为覆盖率的一部分进行跟踪。在审查降低总体代码覆盖率的 pull request 时,将考虑这些限制。

安装

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 提供预编译的二进制文件,这可能会导致一些符号问题。如果这是一个问题,请使用 --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 文件以获取完整许可证。