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
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:)
来检索 width
和 height
值。
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)
Partial 有一个完整的测试套件,该套件在 GitHub Actions 上作为 pull request 的一部分运行。所有测试必须通过才能合并 pull request。
代码覆盖率被收集并报告给 Codecov。100% 的覆盖率是不可能的;某些代码行永远不应该被命中,但类型安全是必需的,并且 Swift 不会将 deinit
函数作为覆盖率的一部分进行跟踪。在审查降低总体代码覆盖率的 pull request 时,将考虑这些限制。
要通过 SwiftPM 安装,请将该软件包添加到依赖项部分,并将其作为目标的依赖项。
let package = Package(
...
dependencies: [
.package(url: "https://github.com/JosephDuffy/Partial.git", from: "1.0.0"),
],
targets: [
.target(name: "MyApp", dependencies: ["Partial"]),
],
...
)
要通过 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 安装,请将以下内容添加到您的 Podfile:
pod 'Partial'
然后运行 pod install
。
该项目是在 MIT 许可证下发布的。查看 LICENSE 文件以获取完整许可证。