sindresorhus / defaults Goto Github PK
View Code? Open in Web Editor NEW💾 Swifty and modern UserDefaults
Home Page: https://swiftpackageindex.com/sindresorhus/Defaults/documentation/defaults
License: MIT License
💾 Swifty and modern UserDefaults
Home Page: https://swiftpackageindex.com/sindresorhus/Defaults/documentation/defaults
License: MIT License
Would be nice to have something like the @Published
property wrapper, but that also handles the synchronization of UserDefaults.
struct FooView: View {
@Defaults.Published(.timeZonesInMenu) var arrayOfThings
}
The following example will cause a crash:
let c = Defaults.publisher(.someSetting).first().sink { print($0) }
The crash says:
'NSRangeException', reason: 'Cannot remove an observer <_TtCO8Defaults8Defaults26UserDefaultsKeyObservation 0x2837dad80> for the key path "someSetting" from <NSUserDefaults 0x282cd7a80> because it is not registered as an observer.
The cause seems to be that the addObserver call synchronously tries to cancels itself.
This example might be contrived, but it's the easiest reproducible case. The actual crash I ran into is more involved.
For example, currently, a Codable that is serialized to a String
is stored as "foo"
because we serialize it as JSON. It would be nice to make it just foo
(without the double-quotes).
The following values could be improved when serialized from a Codable:
"foo"
=> foo
String
plist type. We should really use a native plist Array
type.Dictionary
plist type.This is a breaking change, so it would have to be an opt-in and we could make it the default in the next major version (with still a way to opt-out so existing projects can upgrade).
This is not an easy issue. You are expected to have advanced knowledge of Swift and Codable.
The code should work out of the box, but I'm not sure how to set up the Xcode project to target both macOS and iOS. Help wanted 🙌
We don't plan to do this for many years. Just opening an issue so we don't forget to eventually do it.
Hi
I wanna to say few words about CocaoPods spec updates. Every time you change your .podspec to new values you have to increase version. In latest changeset only swift version was changed. In this case no client of 1.0.0 (swift4.2) would get update through pod update Defaults
.
May you bump spec version as well, please?
I got rid of OptionalKey
in b2fdee2, but I was not able to remove NSSecureCodingOptionalKey
as it seems optionals cannot conform to NSSecureCoding
, or maybe it has something to do with NSSecureCoding
being an Objective-C protocol.
Try changing
to:let key = Defaults.NSSecureCodingKey<ExamplePersistentHistory?>("observeNSSecureCodingOptionalKey")
and see the error.
Help wanted 🙏
Seems we could also support NSNumber
and various Int*
types.
Even arrays of values (#47).
We could also adopt the approach of using a protocol to define the natively supported types: https://www.vadimbulavin.com/advanced-guide-to-userdefaults-in-swift/
Currently, we have to wrap the Codable value in an array, since JSONEncoder
can't handle top-level fragments.
This might be possible in the future though, so please comment your opinions here: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750
Defaults/Sources/Defaults/Defaults.swift
Lines 80 to 85 in c78c1b0
Radar: https://openradar.appspot.com/radar?id=4960629655863296
how to use? error: "Instance member 'subscript' cannot be used on type 'Defaults'" when i call Defaults[.quality]
This compiles:
extension Defaults.Keys {
static let key = Key<Bool>("key", default: false)
static let key2 = Key<Int>("key2", default: 1)
}
Defaults.publisher(keys: Defaults.Keys.key, Defaults.Keys.key2)
This does not:
extension Defaults.Keys {
static let key = Key<Bool>("key", default: false)
static let key2 = Key<Int>("key2", default: 1)
}
Defaults.publisher(keys: .key, .key2)
Hello,
I am not sure if it's a bug or I am doing something wrong. I have a setting window that users can change settings in. It uses property wrapper '@default(.key)' for textfields.
However in other piece of code (ViewModel) I am subscribed to changes
Defaults.observe(
keys: .key1, .key2,
options: [] // Initial value is not needed.
) { [weak self] in
guard let self = self else { return }
self.updateValues(array: &self.items)
}
.tieToLifetime(of: self)
It never gets called even though values are properly saved. I have to manually call update viewModel method after each change. Is it a bug or I'm using the API in a wrong way?
Why this example doesn't works?
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: true)
}
print(UserDefaults.standard.bool(forKey: isUnicornMode.name)) //=> true
Current latest release is outdated. Could you draft a new release?
I suggest publishing everything up to 3275717
, and then current master in separate releases (as there are some incompatible changes).
This can be useful when you need to handle changes to multiple keys the same.
Should support optionally giving an array or keys or if not given subscribe to all changes.
Something like this:
Defaults.observeAll<T: Codable>(
_ keys: Defaults.Key<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping () -> Void
) -> DefaultsObservation
Defaults.observeAll<T: Codable>(
suite: UserDefaults = .standard,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping () -> Void
) -> DefaultsObservation
They should include change objects.
It should also prevent infinite recursion if you call, for example, Defaults[.foo] = true
inside the handler
as that would normally emit an event, which would call it again infinitely. If it doesn't make sense as default, at least there should be an option for this.
The suite:
overload would get all the keys in the given suite and listen to those events. It should not use UserDefaults.didChangeNotification
.
I think it could be useful with an overload that listens to all keys defined in Defaults.Keys
.
Feedback wanted.
Good afternoon and thanks for this amazing package. I came across a small error, I can not get the view to be updated when I reset the value to default. When I close the application and open it again, I see that the value is written and is equal to the default.
class SessionStore: ObservableObject {
@Default(.loginModel) var loginModel
}
extension Defaults.Keys {
static let loginModel = Key<LoginModel?>("loginModel", default: nil)
}
Button(action: {
sessionStore.loginModel = nil
}) {
HStack {
Image(systemName: "flame")
.frame(width: 24)
Text("Выйти из аккаунта")
}.foregroundColor(.red)
}
Hi there,
I'm using a combine publisher like this
private var myPublisher: NonFailingPublisher<Foo?> {
Defaults
.publisher(<FooName>, options: [.initial, .new])
.compactMap { $0.newValue }
.eraseToAnyPublisher()
}
the output is assigned (through some mapping) to a text
in a UILabel
.
But when executed Defaults crashes in Observation+Combine l.36:
private func observationCallback(_ change: BaseChange) {
_ = subscriber?.receive(change)
}
reason: Thread 1: Fatal error: Invalid state: Received value before receiving subscription
Maybe this issue is helpful (same error reason): CombineCommunity/RxCombine#6
One (hacky) way to fix this could be to dispatch _ = subscriber?.receive(change)
to the next run loop via DispatchQueue.main.async { ... }
We currently have to define the key name twice:
static let key = Defaults.Key<Bool>("key", default: false)
Would be nice if we could infer the string "key"
from the static let key
name. I've not managed to find a way to do this, but help welcome.
Any string-keyed dictionary with the value type of any of the supported types.
Will either require #52 or some built-in custom handling.
I attempted to add Defaults through Xcode's new SPM support, but hit an error:
Source files for target Defaults should be located under 'Sources/Defaults', or a custom sources path can be set with the 'path' property in Package.swift
It looks like Xcode isn't happy that the source files are directly in Sources
rather than nested.
I struggle to understand why they are even options? Why can't they always be included? I guess maybe memory concerns, but that should really not be a concern today.
https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions
I'm thinking we could make a Defaults.ObservationOptions
struct that only includes .initial
and .prior
.
when using a key which keeps an optional String like
static let optionalString = Key<String?>("optionalString", default: nil)
using defaults directly gives nil
, as expected:
print(Defaults[.optionalString]) // nil
but using a Combine publisher emits "(null)" as String:
Defaults.publisher(.optionalString).sink { print($0) }.store(in: ...) // KeyChange<Optional<String>>(kind: __C.NSKeyValueChange, indexes: nil, isPrior: false, newValue: Optional("null"), oldValue: nil)
I would expect newValue
to be nil
as well
... which is extremely useful for GUI. An "emulation" instead using kvc would also suffice for most purposes, I think.
~/dev/oss/defaults main
❯ pod trunk push --allow-warnings
[!] Found podspec `Defaults.podspec`
Updating spec repo `trunk`
CocoaPods 1.10.1 is available.
To update use: `sudo gem install cocoapods`
For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.10.1
Validating podspec
-> Defaults (4.2.0)
- WARN | url: The URL (https://twitter.com/sindresorhus) is not reachable.
- NOTE | xcodebuild: note: Using new build system
- NOTE | xcodebuild: note: Building targets in parallel
- NOTE | xcodebuild: note: Using codesigning identity override:
- NOTE | xcodebuild: note: Planning build
- NOTE | xcodebuild: note: Constructing build description
- ERROR | xcodebuild: Returned an unsuccessful exit code. You can use `--verbose` for more information.
- NOTE | xcodebuild: note: Using codesigning identity override: -
- NOTE | xcodebuild: warning: Skipping code signing because the target does not have an Info.plist file and one is not being generated automatically. (in target 'App' from project 'App')
- ERROR | [iOS] xcodebuild: Defaults/Sources/Defaults/Observation.swift:451:20: error: 'NSSecureCodingKeyChange' is only available in iOS 11.0 or newer
- NOTE | xcodebuild: Defaults/Sources/Defaults/Observation.swift:451:1: note: add @available attribute to enclosing extension
- ERROR | [iOS] xcodebuild: Defaults/Sources/Defaults/Observation.swift:454:20: error: 'NSSecureCodingOptionalKeyChange' is only available in iOS 11.0 or newer
- NOTE | xcodebuild: Defaults/Sources/Defaults/Observation.swift:454:1: note: add @available attribute to enclosing extension
- ERROR | xcodebuild: Defaults/Sources/Defaults/Observation.swift:451:20: error: 'NSSecureCodingKeyChange' is only available in tvOS 11.0 or newer
- ERROR | xcodebuild: Defaults/Sources/Defaults/Observation.swift:454:20: error: 'NSSecureCodingOptionalKeyChange' is only available in tvOS 11.0 or newer
[!] The spec did not pass validation, due to 5 errors.
PR welcome to fix this. I don't intend to waste time on Cocoapods.
I haven't made up my mind yet whether it makes sense to expose a Defaults property wrapper or not. Happy to discuss whether it makes and how it would look like.
We currently wrap all saved values in an array:
Defaults/Sources/Defaults/Defaults.swift
Lines 325 to 327 in 55ffea9
This issue was fixed in macOS 10.15, iOS 13, etc, so it's not needed anymore on those platforms. However, we cannot remove it as it would require us to only support the latest OS versions. Instead, we can introduce a Defaults.useLegacyWrapping
(please suggest better name) property that is true
by default, but new projects that only target the latest OS versions could set this to true to get a slightly nicer output in UserDefaults.
I'm not Carthage or Swift frameworks expert, but shouldn't the initializers be public?
I've installed the library via Carthage on macOS project and I got this compile error:
'Defaults.Key<T>' initializer is inaccessible due to 'internal' protection level
extension Defaults.Keys
{
static let myLovelySetting = Defaults.Key<Double>("myLovelySetting", default: 42.0)
}
https://github.com/sindresorhus/Defaults/releases/tag/v5.0.0-beta.1
This is an open space to provide feedback about the v5 release.
https://www.hackingwithswift.com/articles/55/how-to-use-dynamic-member-lookup-in-swift
Would be nice to refer to keys as Defaults.key
instead of Defaults[.key]
.
Hi
Defaults is awesome lib to manage UserDefaults in Swifty style. Unfortunately, it does not support NSSecureCoding protocol right now. It is useful to work with some Apple-specific classes which support NSSecureCoding only. For example, take a look at NSPersistentHistoryToken
.
IssueHunt has been backed by the following sponsors. Become a sponsor
I'm trying to set a Defaults key to observe it later:
extension Defaults.Keys {
static let deviceCache = Key<Dictionary>("deviceCache", default: [], suite: UserDefaults(suiteName: "/Library/Preferences/com.apple.Bluetooth")!)
}
But after typing this, an error appears and says: '<T where T : Decodable, T : Encodable> (Defaults.Key<T>.Type) -> (String, T, UserDefaults) -> Defaults.Key<T>' requires that 'Any' conform to 'Encodable'
What am I doing wrong?
I very often need to only trigger when the value actually changes. Not just that it's set. It could be set to the same value many times and I don't want a notification for that.
Should probably do #33 first as it will simplify implementing this.
Or maybe it should just be the default behavior?
I want to create a keypair with default value is nil. Could this be done?
Hey, this project seems cool! I'm glad you acknowledged SwiftyUserDefaults and stated the difference. Although I would like to use this solution, we use CocoaPods at work, what do you think about adding CocoaPods installation support? 🙂
You could argue that users could just conform their type to Codable
, but not everything can easily be Codable
. For example, [String: String]
.
If we did this, it could make sense to make built-in types conform to this too (https://www.vadimbulavin.com/advanced-guide-to-userdefaults-in-swift/).
I'm looking for feedback on this.
There's another solution to this we could consider, and that would be to ship with AnyCodable
support.
I just implemented a today widget in one of my apps. I needed to read a preference item in the today widget that was currently stored in my main app. Usually, this would mean having to migrate the key over to the App Groups user defaults suite. However, I'm lazy and instead just went with observing the key and then setting it in the App Groups user defaults suite when changed:
// Constants.swift
extension Defaults.Keys {
static let timeZonesInMenu = Key<[ChosenTimeZone]>("timeZonesInMenu", default: [])
}
// SharedConstants.swift
struct Constants {
static let appGroupId = "XXXX.com.sindresorhus.Dato.shared"
static let sharedDefaults = UserDefaults(suiteName: appGroupId)!
}
extension Defaults.Keys {
static let sharedTimeZones = Key<[ChosenTimeZone]>("sharedTimeZones", default: [], suite: Constants.sharedDefaults)
}
// OtherFile.swift
// Sync time zone changes to the shared item.
Defaults.observe(.timeZonesInMenu) { change in
Defaults[.sharedTimeZones] = change.newValue
}
.tieToLifetime(of: self)
And I was thinking this might be useful for others too.
I'm thinking:
// Key with same name
Defaults.mirror(.key, to: suite)
// Key with different name (where the key is strongly-typed and defined in a shared Constants.swift file or something)
Defaults.mirror(.key, to: suite, as: .sharedKey)
// Key with different name
Defaults.mirror(.key, to: suite, as: "sharedKey")
Would probably need a similar unmirror
method too.
Note that this is not syncing. It's only one way.
Feedback welcome.
https://developer.apple.com/documentation/objectivec/nsobject/keyvalueobservingpublisher
This should reduce some overhead. Right now, they're based on the callback API.
Eventually (when this package can target macOS 10.15), I imagine the callback APIs like Defaults.observe()
could be based on the Combine publishers underneath too.
// @fredyshox In case you're interested in working on this.
.updates()
is better in every way. Having two ways to do the same is also confusing. We cannot remove .observe()
for a long time, but maybe we could add availability annotations so that apps that target macOS 10.15+ (and equivalent iOS/tvOS/watchOS) gets a deprecation message.
https://developer.apple.com/documentation/docc
The only blocker is that there's currently no way to run it on GitHub Pages.
Help welcome to add more tests :)
Would be nice to support Combine for the observation API here. This should be in addition to the observation API, not replacing it.
https://developer.apple.com/documentation/combine/receiving_and_handling_events_with_combine
The code should be fully documented, tests added, and documented in the readme too.
This issue requires you to have advanced Swift knowledge and have studied the Combine docs.
IssueHunt has been backed by the following sponsors. Become a sponsor
Sometimes you need to change a structure, for example, add a new property to a struct that is saved in user defaults. You can already do this without migrations by making the property optional, but that complicates a lot of code, and it doesn't work for more comprehensive changes.
I don't have a clear idea of how this would work, but I'm thinking the user would register migrations that run as early as possible at startup or on the first Defaults
call if before that.
Defaults.migrations = [
.Migration(version: "1.2.3") {
// How to handle it here?
}
]
I'm open to ideas.
I have:
extension Defaults.Keys {
static let refreshInterval = Key<TimeInterval?>("refreshInterval")
}
And I would like to specify an initial value, meaning one that is used before any value is set. This is different from default
, which is used whenever the value is nil
, which would not work here as it would make it impossible to set the value to nil
, which is the whole point of making the value optional.
What I'm thinking:
extension Defaults.Keys {
static let refreshInterval = Key<TimeInterval?>("refreshInterval", initialValue: 3600)
}
The problem is that there's no good way to differentiate whether a key unset or nil
.
object(forKey:)
returns nil
whether it's unset or set to nil
.suite.dictionaryRepresentation().keys.contains(key)
returns nil
whether it's unset or set to nil
.suite.persistentDomain(forName:).keys.contains(key)
would work, but we only have the suite instance, not the suite name. And the suite instance doesn't expose the suite name...CFPreferencesCopyValue
returns the true value ignoring the suite.register
defaults, but it requires a suite name.Possible solutions:
suiteName
parameter too if you want to use initialValue
with a non-standard suite. (and use CFPreferencesCopyValue
)UserDefaults.didChangeNotification
triggers for any kind of user defaults change in the process. Even for included SDK or even Apple's APIs. This makes it both inefficient and dangerous. I just had an infinite loop where I was listening to UserDefaults.didChangeNotification
and in the handler, it was setting button.imagePosition
and apparently setting that properly causes a userdefault to be set internally, which resulted in an infinite loop.
I think it would be better to only listen to the keys we know about.
Since these publisher are a combination of publishers, users cannot use . removeDuplicates()
directly on these publishers, but we can do so internally.
Should it be true by default?
I have encountered a need where I would like to create a new key where the value is derived from one or more existing keys.
For example, let's say you have a .name
and .email
key. You might want to simplify the checking and have a .hasFilledOutAllFields
key. This key would be derived from the former keys.
The reason it has to be in Defaults
and not just in a custom extension is that it needs to be reactive. So that, if either .name
or .email
changes, .hasFilledOutAllFields
triggers an update too.
My initial thoughts are something like this:
extension Defaults.Keys {
static let hasFilledOutAllFields = Key<Bool>(derivedFrom: .name, .email) { name, email in
!name.isEmpty && !email.isEmpty
}
}
Another use-case is needing the derived key to be settable too. Not sure how that would look like though.
And maybe it would be useful to be able to listen to external events too. For example, for the .hasFilledOutAllFields
key, we might also need to ensure the keyboard is not shown or something.
Maybe there's a more general way we could handle this. I'm open to ideas.
There's really no reason not to use Swift Package Manager. I don't want the overhead of other package managers.
I plan to drop support for these in the next major Defaults version.
Given
extension Defaults.Keys {
static let source = Defaults.OptionalKey<RssSource>("source")
}
if I assign nil
to defaults[.source]
the value does not get "saved", reading defaults[.source]
gives me the previously assigned non-nil value.
NSSecureCodingKey
and NSSecureCodingOptionalKey
.
Key
instead.Array
is now stored as a native UserDefaults array instead of being stored as a JSON stringified string. Same with Set
and Dictionary
.Codable
. (Existing usage requires migration)NSColor
and UIColor
.Defaults.Toggle
.A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.