Sharing: A Simple Yet Powerful Swift Library
I recently renewed my Point-Free subscription and watched their tour of the Sharing library. It immediately stood out as a neat, easy-to-use, and highly versatile tool. One of its biggest strengths is that it allows you to define persistent properties with different persistence mechanisms—whether .inMemory, .appStorage, or even .fileStorage and is cross-platform.
Before using Sharing, I had to manage a separate library for interacting with values from UserDefaults. This setup not only added extra complexity and boilerplate code but also increased the maintenance burden, requiring constant updates and extra logic just to manage simple persistent properties.
With Sharing, managing persistent data is much simpler. Instead of managing a library that interacts with UserDefaults, I now just define a StoredKey and declare the variable wherever I need it—inside a class, structure, method, etc and it is thread-safe.
Code Examples
Foundation Approach (using UserDefaults)
let showsTimixGPTSuggestionKey = "showsTimixGPTSuggestion"
let userDefaults = UserDefaults.standard
// Read a Value
state.showsTimixGPTSuggestion = userDefaults.bool(forKey: showsTimixGPTSuggestionKey)
// Write a Value
state.showsTimixGPTSuggestion = false
userDefaults.set(false, forKey: "showsTimixGPTSuggestion")
// Observe a Change
let observer = NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { _ in
state.showsTimixGPTSuggestion = userDefaults.bool(forKey: showsTimixGPTSuggestionKey)
}
Sharing Approach (using @Shared)
// Read a Value
@Shared(.showsTimixGPTSuggestion) var showsTimixGPTSuggestion
// Write a Value
$showsTimixGPTSuggestion.withLock { $0 = false }
// - Observe a Change
// Combine
$showsTimixGPTSuggestion.publisher
.sink { state.showsTimixGPTSuggestion = $0 }
.store(in: &cancellables)
// Await
for await value in $showsTimixGPTSuggestion.publisher.eraseToAsyncStream() {
state.showsTimixGPTSuggestion = value
}
A SharedKey defines the identifier, value type and the persistence mechanism used to store the value in one place.
Following example shows how to declare a SharedKey that holds a value of type Bool in appStorage (aka UserDefaults) and has the default value true:
public extension SharedKey where Self == AppStorageKey<Bool>.Default {
static var showsTimixGPTSuggestion: Self {
Self[.appStorage("showsTimixGPTSuggestion"), default: true]
}
}
Pros:
- Type-safe: Ensures type safety at compile time, so you always use the correct type.
- Thread-safe: Preventing race conditions when accessing or updating properties.
- Storage Options: Allowing you to store values in
UserDefaults, files, or memory. - Auto-Sync: The value of the variable is always in sync with the value in the storage.
- Supports default values: providing sensible fallbacks.
- Works seamlessly with The Composable Architecture (TCA).
Cons:
- It introduces another dependency to your project.
Solved: Saving Custom Types to .appStorage
It’s generally not recommended to store custom types in UserDefaults, but if needed, Sharing makes it possible. If you’re dealing with custom types, ensure they conform to Codable to encode and decode them properly. And add a conformance to RawRepresentable with rawValue either String or Data.
In this example the value type is Array<TimerTrigger>? where TimerTrigger conforms to Codable.
// Define a key `SharedKey` that will store an value of type `Array<TimerTrigger>?` with default value as `nil`
public extension SharedKey where Self == AppStorageKey<Array<TimerTrigger>?>.Default {
static var defaultEndTriggers: Self {
Self[.appStorage("defaultEndTriggers"), default: nil]
}
}
// Conform `Array<TimerTrigger>` to `RawRepresentable`
extension Array: @retroactive RawRepresentable where Element == TimerTrigger {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8) else { return nil }
self = (try? JSONDecoder().decode(Self.self, from: data)) ?? []
}
public var rawValue: String {
(try? JSONEncoder().encode(self)).flatMap { String(data: $0, encoding: .utf8) } ?? ""
}
}
Conclusion
The Sharing library is a fantastic addition to any Swift developer’s toolkit. It lets you think about persistent data as simple independent variable rather than worrying about persistence logic. If you’re looking to simplify state management, reduce boilerplate, and ensure type-safe persistence, Sharing is worth a try!
