SwiftUI Dynamic Property with KeychainStorage example

Safwen Debbichi
Bforbank Tech
Published in
5 min readFeb 13, 2024

--

Introduction

You already know how powerful are the most used SwiftUI’s Property Wrappers like @State, @StateObject, @ObservedObject. And you know why they are that powerful: they help us build responsive user interface by making views respond to the changes of the data.

But do you know what do they have in common ? They all conform to DynamicProperty protocol.

Based on Apple’s Documentation, the DynamicProperty protocol has a method called update(). This method is called every time SwiftUI tries to redraw the view.

The DynamicProperty protocol allows us to create custom Dynamic Property Wrappers that can trigger view updates and that’s amazing !

In the following examples we will see case studies of how SwiftUI allows us to do fun stuff.

Case Study #1

SwiftUI provides a Property Wrapper that conforms to DynamicProperty that allows us to listen to UserDefaults changes called @AppStorage. In the following example i will try to create a custom Dynamic Property Wrapper that does the same thing as @AppStorage but for Keychain.

import KeychainWrapper

@propertyWrapper
struct KeychainStorage<T: Codable>: DynamicProperty {

@State private var value: T?
private let key: String
private let store: KeychainWrapper = KeychainWrapper.default

var wrappedValue: T? {
get { value }
nonmutating set {
if let newValue {
store.set(newValue, forKey: key)
} else {
store.removeObject(forKey: key)
}
value = newValue
}
}

var projectedValue: Binding<T?> {
Binding {
wrappedValue
} set: { newValue in
wrappedValue = newValue
}

}

init(key: String) {
self.key = key
_value = State(initialValue: store.object(of: T.self, forKey: key))
}
}

In the code above i created a Property Wrapper called KeyChainStorage that conforms to DynamicProperty protocol. The Property Wrapper has a @State value to be able to mutate it’s value: it works as a mirror as we all know that Structs are value types, therefore the setter of the wrappedValue is nonmutating. In fact, if i remove the @State, i will have an error : Cannot assign to property: ‘self’ is immutable

When we assign a variable inside a struct as @State we change its storage from the struct it self to the SwiftUI stored copy.

The question is why we need to use @State ? … Well because we don’t need to change the value of the Struct itself but instead read and write the value that is managed by SwiftUI. So we are not modifying the value of the struct but the SwiftUI copy of the @State value.

I also imported KeychainWrapper swift package to handle Keychain management. I added a keychain wrapper instance and i use it to set the value when the wrappedValue is being set.

The projectedValue here is a Binding to be able to trigger navigation later.

So every time the wrappedValue is being set to a value, the value is being stored in the keychain automatically. and the fact that my Propery Wrapper conforms to DynamicProperty, every time it’s @State value changes, it triggers a View render and thus a UI update.

struct LoginCredentials: Identifiable, Codable {
let id: UUID
let timestamp: TimeInterval
init(timestamp: TimeInterval) {
self.id = UUID()
self.timestamp = timestamp
}
}
import SwiftUI

struct ContentView: View {

@KeychainStorage(key: "Login_Crendetials") var loginCredentials: LoginCredentials?
@State var text: String = ""

var body: some View {
VStack {
TextField("Pass", text: $text)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
.padding(.horizontal, 100)
.padding(.bottom)
Button(action: {
loginCredentials = .init(timestamp: Date().timeIntervalSince1970)
}, label: {
Text("Login")
}).disabled(text.isEmpty)
}.fullScreenCover(item: $loginCredentials, content: { value in
VStack {
Text("Logged in at timestamp: \(Date(timeIntervalSince1970: value.timestamp).formatted())")
.padding(.bottom)
Button {
loginCredentials = nil
} label: {
Text("Logout")
}
}
})
}
}

In the code above, i have a view that uses the @KeychainStorage to save login credentials. For the sake of the example, i only store the login timestamp.

Once a user taps his passcode and then taps on the login button, the credentials are automatically stored in the Keychain and the fullScreenCover navigation is automatically triggered. (thats why i used a projectedValue 😜)

Now i can read and write on the Keychain and my View is being updated.

Example #1 Demo

Case Study #2

In the following example i created a Property Wrapper with Coordinator Pattern. The Property Wrapper’s wrappedValue is of type NavigationPath which is a struct that has an id of enum type and a coordinator instance.

I know that the Coordinator pattern is not really compatible with SwiftUI

enum ContentViewNavigation: NavigationValue {
case firstView
case secondView
}

struct NavigationPath<N: NavigationValue>: Hashable {
let id: N
let coordinator: any Coordinator

}
@propertyWrapper
struct Navigation<N: NavigationValue>: DynamicProperty {
@State var value: NavigationPath<N>?

var wrappedValue: NavigationPath<N>? {
get { value }
nonmutating set {
value = newValue
}
}

var projectedValue: Binding<NavigationPath<N>?> {
Binding {
wrappedValue
} set: { newValue in
wrappedValue = newValue
}
}
}

Now i created a Coordinator protocol :

protocol Coordinator {
associatedtype Destination: View
@ViewBuilder func start() -> Destination
}

And here is the View part :

import SwiftUI

struct ContentView: View {
@Navigation<ContentViewNavigation> var navigation: NavigationPath?
var body: some View {
NavigationStack {
VStack(spacing: 40, content: {
Button(action: {
navigation = .init(id: .firstView, coordinator: FirstViewCoordinator())
}, label: {
Text("FirstView")
})

Button(action: {
navigation = .init(id: .secondView, coordinator: SecondViewCoordinator())
}, label: {
Text("SecondView")
})
})
.navigationDestination(path: $navigation)
}
}
}

So every time i set the wrappedValue of my navigation , the navigation is triggered through my custom NavigationViewModifier:

extension View {
func navigationDestination<N: NavigationValue>(path: Binding<NavigationPath<N>?>) -> some View {
modifier(NavigationViewModifier(path: path))
}
}

struct NavigationViewModifier<N: NavigationValue>: ViewModifier {
@Binding var path: NavigationPath<N>?
func body(content: Content) -> some View {
content.navigationDestination(item: $path) { value in
AnyView(value.coordinator.start())
}
}
}
Example #2 Demo

Conclusion

The Dynamic Property is the key for all the awesome Property Wrappers like @State, @AppStorage, @StateObject, @ObservedObject … that help us to create responsive UI. It also allows us to create our custom Dynamic Property Wrappers which makes the code cleaner and more scalable.

KeychainWrapper: https://github.com/puretears/KeychainWrapper

--

--