Adding custom Property Wrapper in Swift

Bruno Lorenzo
4 min readOct 17, 2023

--

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?

  1. Add@propertyWrapper annotation in any struct or class.
  2. 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:

  1. saveItem(_:itemClass:key:)
  2. retrieveItem(ofClass:key:)
  3. updateItem(with:ofClass:key:)
  4. 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:

  1. A key to identifying the value we want to store.
  2. The ItemClass that we're going to use in the Keychain.
  3. 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

Have any questions? Feel free to drop me a message! 🙂

  • 🤓 Join me on X for regular content on iOS development tips and insights
  • 🚀 Check out my GitHub where I share all my example projects

--

--

Bruno Lorenzo
Bruno Lorenzo

Written by Bruno Lorenzo

Software Engineer | Innovation Manager at www.houlak.com | Former iOS Tech Lead | I write about iOS, tech, and producitivy

No responses yet