Provides concise UserDefaults storage with Combine publishing capability.
All regular UserDefaults
compatible types and Codable
types are supported (optional or not).
@PDefaults("user.name")
var name = "Pitt"
let cancellable = $name.sink {
print("Name: \($0)")
}
name = "François"
Prints:
Name: Pitt
Name: François
The value "François"
is stored with key "user.name"
in UserDefaults.standard
and will be the value of the property name
from now on even after killing the app.
Store in another suite:
// Remember kids: heroes don't do force unwrap!
let appGroupSuite = UserDefaults(suiteName: "com.company.appgroup")!
@PDefaults("user.name", storage: appGroupSuite)
var name = "Pitt"
You can easily migrate between PDefaults
instances
let appGroupSuite = UserDefaults(suiteName: "com.company.appgroup")!
class Service {
@PDefaults("user.name") private var legacyName = "Pitt"
@PDefaults("user.first-name", suite: appGroupSuite) var name = "Pete"
init() {
_legacyName.migrate(to: _name)
}
}
The migration will be performed only if there's indeed a stored value in the source PDefaults
.
Once the migration is performed, the source PDefaults
is reset, removing its stored value, and guaranteeing that
the migration won't be performed again.
You can add a mapping to convert your source value:
let appGroupSuite = UserDefaults(suiteName: "com.company.appgroup")!
class Service {
@PDefaults("count") private var legacyCount = Double(1)
@PDefaults("index", suite: appGroupSuite) var index = 0
init() {
_legacyCount.migrate(to: _index, { Int($0) - 1 })
}
}
You can mock an instance by setting its mock
property to true
.
@PDefaults("user.name")
var name = "Pitt"
_name.mock = true
Then:
- the underlying suite will never be read or written
- the app group sharing will obviously not work
Note that the instance will ignore the stored value only if the mock flag is set before any access to the wrapped value or the projected value.
You can also mock all instances using the global configuration mock
property.
PDefaultsConfiguration.mock = true
Designed so that reading the value from UserDefaults
is performed maximum once per property and app session.
Thus there's no UserDefaults
or decoding overhead when reading frequently. The counterpart is that the last read or written value is always in memory.
There's currently no way to expose the publisher and the property in one line but you can still make it very concise and readable.
protocol ServiceProtocol {
var name: String { get set }
var namePublisher: AnyPublisher<String, Never> { get }
}
class Service: ServiceProtocol {
@PDefaults("user.name")
var name: String = "Pitt"
var namePublisher: AnyPublisher<String, Never> { $name }
}
To avoid confusion, it is recommended to set the default value to
nil
for optional types.
The initial value given to the property is the default value. The default value will be exposed until you set a non-nil value. When setting a nil value, the default value will be used.
@PDefaults("user.name")
var name: String? = "Pitt"
let cancellable = $name.sink {
print("New name: \($0)")
} // Prints Pitt
name = "François" // Prints François
name = nil // Prints the default value: Pitt
Apps of the same app group can share preferences through UserDefaults
using suites named after the app group
identifier.
PDefaults
plays well with this. If you change the stored value in one app and if any of the other apps in the group is
running and has a PDefaults
instance on the right key, its publisher will be triggered with the new value.
In app A:
// Remember kids: heroes don't do force unwrap!
let appGroupSuite = UserDefaults(suiteName: "com.company.appgroup")!
@PDefaults("user.name", suite: appGroupSuite)
var name = "Pitt"
let cancellable = name.sink {
print("Published: \($0)")
}
In app B:
// Remember kids: heroes don't do force unwrap!
let appGroupSuite = UserDefaults(suiteName: "com.company.appgroup")!
@PDefaults("user.name", suite: appGroupSuite)
var name = "Pitt"
name = "François"
Then app A prints:
Published: François
PDefaults
behaves like CurrentValueSubject
by default.
@PDefaults("user.name")
var name = "Pitt"
let cancellable = $name.sink {
print("Published: \($0) - Property: \(name)")
}
name = "François"
name = "Hubert"
Prints :
Published: Pitt - Property: Pitt
Published: François - Property: François
Published: Hubert - Property: Hubert
You can make PDefaults
behave like @Published
by setting the behavior
parameter to .willSet
.
It will then publishing any new value before exposing it as its wrapped value.
@PDefaults("user.name", behavior: .willSet)
var name = "Pitt"
let cancellable = $name.sink {
print("Published: \($0) - Property: \(name)")
}
name = "François"
name = "Hubert"
Prints :
Published: Pitt - Property: Pitt
Published: François - Property: Pitt
Published: Hubert - Property: François
It's an antipattern. But if you still go with multiple instances:
@PDefaults("user.name", behavior: .didSet)
var name = "Pitt"
@PDefaults("user.name", behavior: .didSet)
var otherName = "Pitt"
Then everything will run smoothly. Value changes on one instance will trigger the other instance's publisher, and the wrapped values will be the same, still honoring each instance behavior independently.
Note that it introduces a small overhead as decoding will occur in both instances when necessary.
Also it's an antipattern, you can easily create infinite loops if one's publisher triggers the other's update.
iOS 13.0+, macOS 12+
PDefaults is available via Swift Package Manager using this repository URL.
PDefaults is available under the MIT license. See the LICENSE file for more info.