FTPropertyWrappers 2.0.0

FTPropertyWrappers 2.0.0

Futured 维护。



FTPropertyWrappers logo

FTPropertyWrappers

Swift

包含我们在项目中常用的一组封装的包。本包包含对 UserDefaults、Keychain、StoredSubject 和同步的属性封装。

安装

当使用 Swift 包管理器时,可以使用 Xcode 11+ 版本的安装,或者添加以下行到依赖关系中

.package(url: "https://github.com/futuredapp/FTPropertyWrappers.git", from: "2.0.0")

当使用 CocoaPods 时,请将以下行添加到 Podfile

pod 'FTPropertyWrappers', '~> 1.0'

功能

本包的主旨是为程序员提供以尽可能简单的 API 访问常用功能或代码片段。运行时效率虽然重要,但本包并非主要关注。截至目前,该包包含以下功能的封装

  • Serialized 是基于 Dispatch 的属性简单实现的在某些线程上存在的属性
  • GenericPasswordInternetPassword 是可以在 Keychain 中存储的类实现,具有检查特定密钥链项属性的能力

用法

序列化

序列化是根据dispatch(GCD)实现的线程局部属性的简单实现。初始化时创建特殊线程,所有读写操作都在该线程上执行。默认情况下,读写操作是阻塞的,因此如果您使用读写操作(如Int的+=),则会发送两个阻塞操作。

用户可以为线程提供自定义标签。

@Serialized var number: Int = 20
@Serialized(customQueue: "my.queue.identifier") var otherNumber: Int = 30

如果您想执行多个操作,可以使用asyncAccess(transform:)方法避免发送多个同步操作(以及可能的死锁)。

_number.asyncAccess { current -> Int in
    var someAggregator = current
    for 0...10 {
        someAggregator += current
    }
    return someAggregator
}

GenericPassword

通用密码是财产包装器,使得可能将数据作为kSecClassGenericPassword密钥项类存储在密钥链中。这允许存储任何Codable数据类型,包括单个值(如IntString)。我们的实现还有一些高级功能,如检查和修改属性。然而,主要目的是避免用户承受不必要的语法负担。只需记住,某些属性,如service,是为了在密钥链中识别数据并为稳定的属性包装器API提供所必需的。

@GenericPassword(service: "my.service") var myName: String?
myName = "Peter Parker"
@GenericPassword(service: "my.service") var otherProperty: String?
print(otherProperty) // prints Optional("Peter Parker")

如您所见,财产是在访问时加载和存储的。可以禁用此类行为。但是,如果您想检查和修改密钥项的属性,如例如comment,您需要手动加载密钥项并手动存储它。由于C语言API的限制,一旦设置了属性,我们就无法重置(删除)它。您需要删除并重新将项插入密钥链中。

@GenericPassword(service: "my.service") var myName: String?
try _myName.loadFromKeychain()
_myName.comment = "This is name of a secret hero! Do not show it on public!"
try _myName.saveToKeychain()

如果您想从密钥链中删除项,只需将包装的属性设置为nil并将其保存到密钥链中。您也可以手动删除该项。

@GenericPassword(service: "my.service") var myName: String?
myName = nil // Deletes immediately since myName is saved upon access
try _myName.saveToKeychain() // Deletes since wrapped property is nil
try _myName.deleteKeychain() // Explicit delete.

访问控制允许您指定在授予对密钥项数据的访问之前应该使用哪种认证方法。例如,应用程序可以创建自己的密码或要求生物识别认证。通用密码包装器允许您修改项的访问参数。有两种可能的方法。在每个写操作之前定义新的访问控制修饰符或为包装器实例定义默认的访问控制参数。在后一种情况下,当kSecAccessControl属性为nil时,将在保存时实例化访问控制修饰符。这可能导致异常,从而终止保存操作。此存储库中示例项目的示例中可以找到具有访问控制的通用密码的示例。

// Example declaration of GenericPassword with access control from exaple project
@GenericPassword(
    service: "app.futured.ftpropertywrappers.example.name",
    account: "[email protected]",
    refreshPolicy: .manual,
    accessOption: kSecAttrAccessibleWhenUnlocked,
    accessFlags: [.biometryAny, .or, .devicePasscode]
) var data: Hidden?

Biometry example

内部,所有钥匙串属性封装都使用编码器,以特定的方式对单个值类型进行编码(详细信息请参考 KeychainEncoderKeychainDecoder 结构)以及键值类型或集合使用二进制 Plist。然而,在不需要默认编码的情况下,使用类型 Data 作为泛型类型将提供用户以加载和存储在钥匙串中的原始数据。例如,使用此方法存储或加载 Utf16 编码的字符串或 JSON 编码的键值容器。

@GenericPassword(service: "my.service") var myData: Data?

InternetPassword

互联网密码是面向存储和组织各种互联网服务密码的钥匙串项目类。它仍具较大的优势,但缺乏生物识别认证支持。

@InternetPassword(
    server: "my.server",
    account: "my.account",
    domain: "my.domain",
    aProtocol: kSecAttrProtocolSSH,
    authenticationType: kSecAttrAuthenticationTypeHTMLForm,
    port: 8080,
    path: "/a/b/c"
) var myPassword: String?

请注意,上述示例中的每个参数都是“主键”的一部分,省略任何参数都可能引起歧义。以下示例将演示两个具有不同声明但只在一个记录中存储在钥匙串中的属性封装。

@InternetPassword(
    server: "my.server",
    account: "my.account",
    domain: "my.domain",
    aProtocol: kSecAttrProtocolSSH,
    authenticationType: kSecAttrAuthenticationTypeHTMLForm,
    port: 8080,
    path: "/a/b/c"
) var declA: String?
@InternetPassword(
    server: "my.server",
    account: "my.account"
) var declB: String?

declA = "The Valley Wind"
print(declB) // Prints Optional("The Valley Wind")

但是不同运行顺序的属性交换会产生不同的结果。

declB = "The Valley Wind"
print(declA) // Prints nil

让我们考虑第三种情况,其中有一个名为 declC 的属性,它在 aProtocol 属性上与 declA 不同。这将导致钥匙串中出现两个不同的记录。declB 将显示哪个值?

@InternetPassword(
    server: "my.server",
    account: "my.account",
    domain: "my.domain",
    aProtocol: kSecAttrProtocolFTP,
    authenticationType: kSecAttrAuthenticationTypeHTMLForm,
    port: 8080,
    path: "/a/b/c"
) var declc: String?

declC = "The Sixth Station"
declA = "The Valley Wind"
print(declB) // Prints Optional("The Sixth Station")
try _declC.deleteKeychain()
print(declB) // Prints Optional("The Valley Wind")

似乎在出现歧义的情况下,选择具有最老 creationDate 的元素作为结果。此声明不具有文档依据,但已在单元测试中得到测试。同样,其他钥匙串项目类也适用。

迁移说明

2.0.0

@UserDefaults 被移除,取而代之的是 iOS 14+ 中的 @AppStorage@StoredSubject 被移除,取而代之的是 iOS 13+ 中的 CurrentValueSubject

1.0.0

在此迁移过程中,只有钥匙串属性封装做出了破坏性代码更改。其他更改都是添加性的。为了成功地迁移您的钥匙串相关代码,您需要执行以下四个步骤。

  1. 将所有 KeychainStoreCodableKeychainAdapterKeychainAdapter 变量更改为 @GenericPassword
  2. 您提供给旧实现的 key 代表新实现中 GenericPasswordaccount
  3. 旧实现中的 service 特性由 serviceIdentifier 代表。如果您使用 CodableKeychainAdapter.defaultDomain,则其值为 Bundle.main.bundleIdentifier! + ".securedomain.default"
  4. 如果您为复合类型使用 CodableKeychainAdapter 的编码实现,则您需要自行提供解码代码,因为新实现使用 Plist 而不是 JSON。只需将类型 Data 作为新 GenericPassword 属性封装的泛型参数,并使用所需的任何编码方法即可。有关详细信息,请参阅上面的内容。

贡献者

当前维护者及主要贡献者是 Mikoláš Stuchlík[email protected]

我们想要感谢其他贡献者,包括

许可证

FTPropertyWrappers 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件