Adding custom Property Wrapper in Swift
Property Wrappers are a powerful and versatile feature introduced in Swift 5.1.
It allows us to add custom behavior to properties. It's an elegant way to manage and manipulate property values. We can use property wrappers to execute some custom logic when the property value is stored, read, or changed.
Let's see how we can create a custom property wrapper, and use it afterward.
How do we create a Property Wrapper?
- Add
@propertyWrapper
annotation in any struct or class. - Provide a stored property
wrappedValue
. Here is where we can add our custom logic.
@propertyWrapper
struct Uppercase {
var wrappedValue: String {
didSet { wrappedValue = wrappedValue.uppercased() }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.uppercased()
}
}
To use it, we just need to add @Uppercase
to the property we want.
struct User {
@Uppercase var fullName: String
}
let user = User(fullName: "John Doe")
print(user.fullName)
// JOHN DOE
Let's see it with a practical example
We have in place a Keychain utility to interact with the Keychain services API.
Here's the article where I covered the implementation details for the API 👇
The utility has 4 methods:
saveItem(_:itemClass:key:)
retrieveItem(ofClass:key:)
updateItem(with:ofClass:key:)
deleteItem(ofClass:key:)
This is very straightforward and saves us from having to interact with the Keychain API.
However, it has a downside. Every time we want to store and read a secure element, we'll end up with some code like this:
// Store:
let apiToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJnb2Fsc2J1ZGR5IiwiZXhwIjo2NDA5MjIxMTIwMH0.JoDuSMARI2Ihh8fisiUxfQiP8AE_WFz9Hcogkk8QMcQ"
do {
try KeychainManager.standard.saveItem(apiToken, itemClass: .generic, key: "ApiToken", attributes: apiTokenAttributes)
} catch let keychainError as KeychainManager.KeychainError {
print(keychainError.description)
} catch {
print(error)
}
// Read:
do {
let token: String = try KeychainManager.standard.retrieveItem(ofClass: .generic, key: "ApiToken")
} catch let keychainError as KeychainManager.KeychainError {
print(keychainError.description)
} catch {
print(error)
}
We can go one step ahead and create a property wrapper to make our code cleaner and more readable.
Our goal will be having something like this:
@KeychainStorage(key: "API-Token", itemClass: .generic)
var token: String?
// Store
token = "c0c61d55558b0c8dac82a16c04981eea7c99e37d714367e575028221028b0d4cff122d6a7556fc0ab1c66d1d4b05b378"
// Delete
token = nil
Defining what we need
For our example, there are three things we're going to need:
- A key to identifying the value we want to store.
- The ItemClass that we're going to use in the Keychain.
- A KeychainManager instance to interact with the Keychain API.
@propertyWrapper
public struct KeychainStorage {
let key: String
let itemClass: ItemClass
let keychain: KeychainManager
var wrappedValue: <#Value#>
init(key: String, itemClass: ItemClass, keychain: KeychainManager = .standard) {
self.key = key
self.itemClass = itemClass
self.keychain = keychain
}
}
The missing part is to determinate what type should be wrappedValue
? It could be any type that we store in the Keychain, so we don't know in advance.
To solve this, we can make our KeychainStorage
generic.
@propertyWrapper
public struct KeychainStorage<T: Decodable> {
let key: String
let itemClass: ItemClass
let keychain: KeychainManager
var wrappedValue: T? { ... }
init(key: String, itemClass: ItemClass, keychain: KeychainManager = .standard) {
self.key = key
self.itemClass = itemClass
self.keychain = keychain
}
}
Now, we need to make the calls to our Keychain utility API to return and set the wrappedValue
.
@propertyWrapper
public struct KeychainStorage<T: Codable> {
let key: String
let itemClass: ItemClass
let keychain: KeychainManager
public var wrappedValue: T? {
get {
return getItem()
}
set {
if let newValue {
getItem() != nil
? updateItem(newValue)
: saveItem(newValue)
} else {
deleteItem()
}
}
}
public init(key: String, itemClass: ItemClass, keychain: KeychainManager = .standard) {
self.key = key
self.itemClass = itemClass
self.keychain = keychain
}
}
// MARK: - Helpers
private extension KeychainStorage {
func getItem() -> T? {
do {
return try keychain.retrieveItem(ofClass: itemClass, key: key)
} catch {
handleError(error)
}
return nil
}
func saveItem(_ item: T) {
do {
try keychain.saveItem(item, itemClass: itemClass, key: key)
} catch {
handleError(error)
}
}
func updateItem(_ item: T) {
do {
try keychain.updateItem(with: item, ofClass: itemClass, key: key)
} catch {
handleError(error)
}
}
func deleteItem() {
do {
try keychain.deleteItem(ofClass: itemClass, key: key)
} catch {
handleError(error)
}
}
func handleError(_ error: Error) {
print(error)
}
}
Finally, we can use@KeychainStorage
annotation.
@KeychainStorage(key: "API-Token", itemClass: .generic)
var token: String?
- If an item of type generic with API-Token key is stored in the Keychain,
token
will have that value ready to use. - Every time we set a value to
token
, we will save it to the Keychain (or update it in case the item was already stored). - If we want to delete the item from the keychain, we just need to set the value to
nil
.
In case you're interested, here's the code for the Keychain utility