FTPropertyWrappers logo

FTPropertyWrappers

Swift

包含我们项目中常用的包装器的软件包。该软件包包含用于用户默认设置、钥匙串、StoredSubject 和同步的属性包装器。

安装

使用 Swift Package Manager 时,请使用 Xcode 11+ 安装,或将以下行添加到您的依赖项中

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

使用 CocoaPods 时,将以下行添加到您的 Podfile

pod 'FTPropertyWrappers', '~> 1.0'

特性

此软件包的主要目的是为程序员提供对常用功能或代码片段的访问,并尽可能提供简单的 API。运行时效率虽然重要,但不是此软件包的主要重点。目前,该软件包包含以下功能的包装器

用法

Serialized

Serialized 是基于 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

Generic Password 是属性包装器,它使您可以将数据作为 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: "example@futred.com",
    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

Internet 密码是一种钥匙串项目类,旨在存储和组织各种互联网服务的密码。它充分利用了属性,但缺乏生物特征认证支持。

@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íkmikolas.stuchlik@futured.app

我们要感谢其他贡献者,即

许可证

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