Giter VIP home page Giter VIP logo

defaults's Introduction

Defaults

Swifty and modern UserDefaults

Store key-value pairs persistently across launches of your app.

It uses UserDefaults underneath but exposes a type-safe facade with lots of nice conveniences.

It's used in production by all my apps (1 million+ users).

Highlights

  • Strongly typed: You declare the type and default value upfront.
  • SwiftUI: Property wrapper that updates the view when the UserDefaults value changes.
  • Codable support: You can store any Codable value, like an enum.
  • NSSecureCoding support: You can store any NSSecureCoding value.
  • Observation: Observe changes to keys.
  • Debuggable: The data is stored as JSON-serialized values.
  • Customizable: You can serialize and deserialize your own type in your own way.
  • iCloud support: Automatically synchronize data between devices.

Benefits over @AppStorage

  • You define strongly-typed identifiers in a single place and can use them everywhere.
  • You also define the default values in a single place instead of having to remember what default value you used in other places.
  • You can use it outside of SwiftUI.
  • You can observe value updates.
  • Supports many more types, even Codable.
  • Easy to add support for your own custom types.
  • Comes with a convenience SwiftUI Toggle component.

Compatibility

  • macOS 11+
  • iOS 14+
  • tvOS 14+
  • watchOS 9+
  • visionOS 1+

Install

Add https://github.com/sindresorhus/Defaults in the “Swift Package Manager” tab in Xcode.

Support types

  • Int(8/16/32/64)
  • UInt(8/16/32/64)
  • Double
  • CGFloat
  • Float
  • String
  • Bool
  • Date
  • Data
  • URL
  • UUID
  • Range
  • ClosedRange
  • Codable
  • NSSecureCoding
  • Color 1 (SwiftUI)
  • Color.Resolved 1 (SwiftUI)
  • NSColor
  • UIColor
  • NSFontDescriptor
  • UIFontDescriptor

Defaults also support the above types wrapped in Array, Set, Dictionary, Range, ClosedRange, and even wrapped in nested types. For example, [[String: Set<[String: Int]>]].

For more types, see the enum example, Codable example, or advanced Usage. For more examples, see Tests/DefaultsTests.

You can easily add support for any custom type.

If a type conforms to both NSSecureCoding and Codable, then Codable will be used for the serialization.

Usage

API documentation.

You declare the defaults keys upfront with a type and default value.

The key name must be ASCII, not start with @, and cannot contain a dot (.).

import Defaults

extension Defaults.Keys {
	static let quality = Key<Double>("quality", default: 0.8)
	//            ^            ^         ^                ^
	//           Key          Type   UserDefaults name   Default value
}

You can then access it as a subscript on the Defaults global:

Defaults[.quality]
//=> 0.8

Defaults[.quality] = 0.5
//=> 0.5

Defaults[.quality] += 0.1
//=> 0.6

Defaults[.quality] = "🦄"
//=> [Cannot assign value of type 'String' to type 'Double']

You can also declare optional keys for when you don't want to declare a default value upfront:

extension Defaults.Keys {
	static let name = Key<Double?>("name")
}

if let name = Defaults[.name] {
	print(name)
}

The default value is then nil.

You can also specify a dynamic default value. This can be useful when the default value may change during the lifetime of the app:

extension Defaults.Keys {
	static let camera = Key<AVCaptureDevice?>("camera") { .default(for: .video) }
}

Enum example

enum DurationKeys: String, Defaults.Serializable {
	case tenMinutes = "10 Minutes"
	case halfHour = "30 Minutes"
	case oneHour = "1 Hour"
}

extension Defaults.Keys {
	static let defaultDuration = Key<DurationKeys>("defaultDuration", default: .oneHour)
}

Defaults[.defaultDuration].rawValue
//=> "1 Hour"

(This works as long as the raw value of the enum is any of the supported types)

Codable example

struct User: Codable, Defaults.Serializable {
	let name: String
	let age: String
}

extension Defaults.Keys {
	static let user = Key<User>("user", default: .init(name: "Hello", age: "24"))
}

Defaults[.user].name
//=> "Hello"

Use keys directly

You are not required to attach keys to Defaults.Keys.

let isUnicorn = Defaults.Key<Bool>("isUnicorn", default: true)

Defaults[isUnicorn]
//=> true

SwiftUI support

@Default

You can use the @Default property wrapper to get/set a Defaults item and also have the view be updated when the value changes. This is similar to @State.

extension Defaults.Keys {
	static let hasUnicorn = Key<Bool>("hasUnicorn", default: false)
}

struct ContentView: View {
	@Default(.hasUnicorn) var hasUnicorn

	var body: some View {
		Text("Has Unicorn: \(hasUnicorn)")
		Toggle("Toggle", isOn: $hasUnicorn)
		Button("Reset") {
			_hasUnicorn.reset()
		}
	}
}

Note that it's @Default, not @Defaults.

You cannot use @Default in an ObservableObject. It's meant to be used in a View.

Toggle

There's also a SwiftUI.Toggle wrapper that makes it easier to create a toggle based on a Defaults key with a Bool value.

extension Defaults.Keys {
	static let showAllDayEvents = Key<Bool>("showAllDayEvents", default: false)
}

struct ShowAllDayEventsSetting: View {
	var body: some View {
		Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents)
	}
}

You can also listen to changes:

struct ShowAllDayEventsSetting: View {
	var body: some View {
		Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents)
			// Note that this has to be directly attached to `Defaults.Toggle`. It's not `View#onChange()`.
			.onChange {
				print("Value", $0)
			}
	}
}

Observe changes to a key

extension Defaults.Keys {
	static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}

// …

Task {
	for await value in Defaults.updates(.isUnicornMode) {
		print("Value:", value)
	}
}

In contrast to the native UserDefaults key observation, here you receive a strongly-typed change object.

Reset keys to their default values

extension Defaults.Keys {
	static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}

Defaults[.isUnicornMode] = true
//=> true

Defaults.reset(.isUnicornMode)

Defaults[.isUnicornMode]
//=> false

This works for a Key with an optional too, which will be reset back to nil.

Control propagation of change events

Changes made within the Defaults.withoutPropagation closure will not be propagated to observation callbacks (Defaults.observe() or Defaults.publisher()), and therefore could prevent infinite recursion.

let observer = Defaults.observe(keys: .key1, .key2) {
		// …

		Defaults.withoutPropagation {
			// Update `.key1` without propagating the change to listeners.
			Defaults[.key1] = 11
		}

		// This will be propagated.
		Defaults[.someKey] = true
	}

It's just UserDefaults with sugar

This works too:

extension Defaults.Keys {
	static let isUnicorn = Key<Bool>("isUnicorn", default: true)
}

UserDefaults.standard[.isUnicorn]
//=> true

Shared UserDefaults

let extensionDefaults = UserDefaults(suiteName: "com.unicorn.app")!

extension Defaults.Keys {
	static let isUnicorn = Key<Bool>("isUnicorn", default: true, suite: extensionDefaults)
}

Defaults[.isUnicorn]
//=> true

// Or

extensionDefaults[.isUnicorn]
//=> true

Default values are registered with UserDefaults

When you create a Defaults.Key, it automatically registers the default value with normal UserDefaults. This means you can make use of the default value in, for example, bindings in Interface Builder.

extension Defaults.Keys {
	static let isUnicornMode = Key<Bool>("isUnicornMode", default: true)
}

print(UserDefaults.standard.bool(forKey: Defaults.Keys.isUnicornMode.name))
//=> true

Note A Defaults.Key with a dynamic default value will not register the default value in UserDefaults.

API

Defaults

Defaults.Keys

Type: class

Stores the keys.

Defaults.Key (alias Defaults.Keys.Key)

Defaults.Key<T>(_ name: String, default: T, suite: UserDefaults = .standard)

Type: class

Create a key with a default value.

The default value is written to the actual UserDefaults and can be used elsewhere. For example, with a Interface Builder binding.

Defaults.Serializable

public protocol DefaultsSerializable {
	typealias Value = Bridge.Value
	typealias Serializable = Bridge.Serializable
	associatedtype Bridge: Defaults.Bridge

	static var bridge: Bridge { get }
}

Type: protocol

Types that conform to this protocol can be used with Defaults.

The type should have a static variable bridge which should reference an instance of a type that conforms to Defaults.Bridge.

Defaults.Bridge

public protocol DefaultsBridge {
	associatedtype Value
	associatedtype Serializable

	func serialize(_ value: Value?) -> Serializable?
	func deserialize(_ object: Serializable?) -> Value?
}

Type: protocol

A Bridge is responsible for serialization and deserialization.

It has two associated types Value and Serializable.

  • Value: The type you want to use.
  • Serializable: The type stored in UserDefaults.
  • serialize: Executed before storing to the UserDefaults .
  • deserialize: Executed after retrieving its value from the UserDefaults.

Defaults.AnySerializable

Defaults.AnySerializable<Value: Defaults.Serializable>(_ value: Value)

Type: class

Type-erased wrapper for Defaults.Serializable values.

  • get<Value: Defaults.Serializable>() -> Value?: Retrieve the value which type is Value from UserDefaults.
  • get<Value: Defaults.Serializable>(_: Value.Type) -> Value?: Specify the Value you want to retrieve. This can be useful in some ambiguous cases.
  • set<Value: Defaults.Serializable>(_ newValue: Value): Set a new value for Defaults.AnySerializable.

Defaults.reset(keys…)

Type: func

Reset the given keys back to their default values.

You can also specify string keys, which can be useful if you need to store some keys in a collection, as it's not possible to store Defaults.Key in a collection because it's generic.

Defaults.removeAll

Defaults.removeAll(suite: UserDefaults = .standard)

Type: func

Remove all entries from the given UserDefaults suite.

Defaults.withoutPropagation(_ closure:)

Execute the closure without triggering change events.

Any Defaults key changes made within the closure will not propagate to Defaults event listeners (Defaults.observe() and Defaults.publisher()). This can be useful to prevent infinite recursion when you want to change a key in the callback listening to changes for the same key.

@Default(_ key:)

Get/set a Defaults item and also have the SwiftUI view be updated when the value changes.

Advanced

Defaults.CollectionSerializable

public protocol DefaultsCollectionSerializable: Collection, Defaults.Serializable {
	init(_ elements: [Element])
}

Type: protocol

A Collection which can store into the native UserDefaults.

It should have an initializer init(_ elements: [Element]) to let Defaults do the de-serialization.

Defaults.SetAlgebraSerializable

public protocol DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable {
	func toArray() -> [Element]
}

Type: protocol

A SetAlgebra which can store into the native UserDefaults.

It should have a function func toArray() -> [Element] to let Defaults do the serialization.

Advanced usage

Custom types

Although Defaults already has built-in support for many types, you might need to be able to use your own custom type. The below guide will show you how to make your own custom type work with Defaults.

  1. Create your own custom type.
struct User {
	let name: String
	let age: String
}
  1. Create a bridge that conforms to Defaults.Bridge, which is responsible for handling serialization and deserialization.
struct UserBridge: Defaults.Bridge {
	typealias Value = User
	typealias Serializable = [String: String]

	public func serialize(_ value: Value?) -> Serializable? {
		guard let value else {
			return nil
		}

		return [
			"name": value.name,
			"age": value.age
		]
	}

	public func deserialize(_ object: Serializable?) -> Value? {
		guard
			let object,
			let name = object["name"],
			let age = object["age"]
		else {
			return nil
		}

		return User(
			name: name,
			age: age
		)
	}
}
  1. Create an extension of User that conforms to Defaults.Serializable. Its static bridge should be the bridge we created above.
struct User {
	let name: String
	let age: String
}

extension User: Defaults.Serializable {
	static let bridge = UserBridge()
}
  1. Create some keys and enjoy it.
extension Defaults.Keys {
	static let user = Defaults.Key<User>("user", default: User(name: "Hello", age: "24"))
	static let arrayUser = Defaults.Key<[User]>("arrayUser", default: [User(name: "Hello", age: "24")])
	static let setUser = Defaults.Key<Set<User>>("user", default: Set([User(name: "Hello", age: "24")]))
	static let dictionaryUser = Defaults.Key<[String: User]>("dictionaryUser", default: ["user": User(name: "Hello", age: "24")])
}

Defaults[.user].name //=> "Hello"
Defaults[.arrayUser][0].name //=> "Hello"
Defaults[.setUser].first?.name //=> "Hello"
Defaults[.dictionaryUser]["user"]?.name //=> "Hello"

Dynamic value

There might be situations where you want to use [String: Any] directly, but Defaults need its values to conform to Defaults.Serializable. The type-eraser Defaults.AnySerializable helps overcome this limitation.

Defaults.AnySerializable is only available for values that conform to Defaults.Serializable.

Warning: The type-eraser should only be used when there's no other way to handle it because it has much worse performance. It should only be used in wrapped types. For example, wrapped in Array, Set or Dictionary.

Primitive type

Defaults.AnySerializable conforms to ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByBooleanLiteral, ExpressibleByNilLiteral, ExpressibleByArrayLiteral, and ExpressibleByDictionaryLiteral.

Which means you can assign these primitive types directly:

let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: 1)
Defaults[any] = "🦄"

Other types

Using get and set

For other types, you will have to assign it like this:

enum mime: String, Defaults.Serializable {
	case JSON = "application/json"
	case STREAM = "application/octet-stream"
}

let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: [Defaults.AnySerializable(mime.JSON)])

if let mimeType: mime = Defaults[any].get() {
	print(mimeType.rawValue)
	//=> "application/json"
}

Defaults[any].set(mime.STREAM)

if let mimeType: mime = Defaults[any].get() {
	print(mimeType.rawValue)
	//=> "application/octet-stream"
}

Wrapped in Array, Set, or Dictionary

Defaults.AnySerializable also support the above types wrapped in Array, Set, Dictionary.

Here is the example for [String: Defaults.AnySerializable]:

extension Defaults.Keys {
	static let magic = Key<[String: Defaults.AnySerializable]>("magic", default: [:])
}

enum mime: String, Defaults.Serializable {
	case JSON = "application/json"
}

// …
Defaults[.magic]["unicorn"] = "🦄"

if let value: String = Defaults[.magic]["unicorn"]?.get() {
	print(value)
	//=> "🦄"
}

Defaults[.magic]["number"] = 3
Defaults[.magic]["boolean"] = true
Defaults[.magic]["enum"] = Defaults.AnySerializable(mime.JSON)

if let mimeType: mime = Defaults[.magic]["enum"]?.get() {
	print(mimeType.rawValue)
	//=> "application/json"
}

For more examples, see Tests/DefaultsAnySerializableTests.

Serialization for ambiguous Codable type

You may have a type that conforms to Codable & NSSecureCoding or a Codable & RawRepresentable enum. By default, Defaults will prefer the Codable conformance and use the CodableBridge to serialize it into a JSON string. If you want to serialize it as a NSSecureCoding data or use the raw value of the RawRepresentable enum, you can conform to Defaults.PreferNSSecureCoding or Defaults.PreferRawRepresentable to override the default bridge:

enum mime: String, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable {
	case JSON = "application/json"
}

extension Defaults.Keys {
	static let magic = Key<[String: Defaults.AnySerializable]>("magic", default: [:])
}

print(UserDefaults.standard.string(forKey: "magic"))
//=> application/json

Had we not added Defaults.PreferRawRepresentable, the stored representation would have been "application/json" instead of application/json.

This can also be useful if you conform a type you don't control to Defaults.Serializable as the type could receive Codable conformance at any time and then the stored representation would change, which could make the value unreadable. By explicitly defining which bridge to use, you ensure the stored representation will always stay the same.

Custom Collection type

  1. Create your Collection and make its elements conform to Defaults.Serializable.
struct Bag<Element: Defaults.Serializable>: Collection {
	var items: [Element]

	var startIndex: Int { items.startIndex }
	var endIndex: Int { items.endIndex }

	mutating func insert(element: Element, at: Int) {
		items.insert(element, at: at)
	}

	func index(after index: Int) -> Int {
		items.index(after: index)
	}

	subscript(position: Int) -> Element {
		items[position]
	}
}
  1. Create an extension of Bag that conforms to Defaults.CollectionSerializable.
extension Bag: Defaults.CollectionSerializable {
	init(_ elements: [Element]) {
		self.items = elements
	}
}
  1. Create some keys and enjoy it.
extension Defaults.Keys {
	static let stringBag = Key<Bag<String>>("stringBag", default: Bag(["Hello", "World!"]))
}

Defaults[.stringBag][0] //=> "Hello"
Defaults[.stringBag][1] //=> "World!"

Custom SetAlgebra type

  1. Create your SetAlgebra and make its elements conform to Defaults.Serializable & Hashable
struct SetBag<Element: Defaults.Serializable & Hashable>: SetAlgebra {
	var store = Set<Element>()

	init() {}

	init(_ store: Set<Element>) {
		self.store = store
	}

	func contains(_ member: Element) -> Bool {
		store.contains(member)
	}

	func union(_ other: SetBag) -> SetBag {
		SetBag(store.union(other.store))
	}

	func intersection(_ other: SetBag) -> SetBag {
		var setBag = SetBag()
		setBag.store = store.intersection(other.store)
		return setBag
	}

	func symmetricDifference(_ other: SetBag) -> SetBag {
		var setBag = SetBag()
		setBag.store = store.symmetricDifference(other.store)
		return setBag
	}

	@discardableResult
	mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) {
		store.insert(newMember)
	}

	mutating func remove(_ member: Element) -> Element? {
		store.remove(member)
	}

	mutating func update(with newMember: Element) -> Element? {
		store.update(with: newMember)
	}

	mutating func formUnion(_ other: SetBag) {
		store.formUnion(other.store)
	}

	mutating func formSymmetricDifference(_ other: SetBag) {
		store.formSymmetricDifference(other.store)
	}

	mutating func formIntersection(_ other: SetBag) {
		store.formIntersection(other.store)
	}
}
  1. Create an extension of SetBag that conforms to Defaults.SetAlgebraSerializable
extension SetBag: Defaults.SetAlgebraSerializable {
	func toArray() -> [Element] {
		Array(store)
	}
}
  1. Create some keys and enjoy it.
extension Defaults.Keys {
	static let stringSet = Key<SetBag<String>>("stringSet", default: SetBag(["Hello", "World!"]))
}

Defaults[.stringSet].contains("Hello") //=> true
Defaults[.stringSet].contains("World!") //=> true

FAQ

How can I store a dictionary of arbitrary values?

After Defaults v5, you don't need to use Codable to store dictionary, Defaults supports storing dictionary natively. For Defaults support types, see Support types.

How is this different from SwiftyUserDefaults?

It's inspired by that package and other solutions. The main difference is that this module doesn't hardcode the default values and comes with Codable support.

Maintainers

Former

Related

  • KeyboardShortcuts - Add user-customizable global keyboard shortcuts to your macOS app
  • LaunchAtLogin - Add "Launch at Login" functionality to your macOS app
  • DockProgress - Show progress in your app's Dock icon
  • Gifski - Convert videos to high-quality GIFs on your Mac
  • More…

Footnotes

  1. You cannot use Color.accentColor. 2

defaults's People

Contributors

andergoig avatar fredyshox avatar hank121314 avatar iuriiiaremenko avatar jordibruin avatar kitwtnb avatar larsjk avatar leomehlig avatar maschina avatar ocjvandijk avatar sindresorhus avatar thatsjustcheesy avatar wouter01 avatar yonat avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

defaults's Issues

Mirroring feature

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.

Remove value wrapper

We currently wrap all saved values in an array:

// Some codable values like URL and enum are encoded as a top-level
// string which JSON can't handle, so we need to wrap it in an array
// We need this: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750

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.

Combine publisher with String? emits "(null)"

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

Swift 5 upgrade process

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?

Get rid of `NSSecureCodingOptionalKey`

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

let key = Defaults.NSSecureCodingOptionalKey<ExamplePersistentHistory>("observeNSSecureCodingOptionalKey")
to:

let key = Defaults.NSSecureCodingKey<ExamplePersistentHistory?>("observeNSSecureCodingOptionalKey")

and see the error.

Help wanted 🙏

v5 release notes draft

Breaking

  • Please read the migration guide.
  • Removed NSSecureCodingKey and NSSecureCodingOptionalKey.
    • You can now just use Key instead.
  • Dropped support for Carthage and CocoaPods.
    • If you use either of these, you can still use Swift Package Manager just for this package.

Improvements

  • Added support for more built-in Swift types.
  • Improved the stored representation of many types.
    • For example, Array is now stored as a native UserDefaults array instead of being stored as a JSON stringified string. Same with Set and Dictionary.
  • Enums no longer need to be Codable. (Existing usage requires migration)
  • Added support for storing NSColor and UIColor.
  • Added Defaults.Toggle.

NSSecureCoding support

Issuehunt badges

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 Summary

ybrin ybrin has been rewarded.

Backers (Total: $80.00)

Submitted pull Requests


Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

Make `@Default` usable in an `ObservableObject`

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)
}

Improve stored representation

Issuehunt badges

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:

  • String (for example string enum): "foo" => foo
  • Arrays are currently stored as a serialized string in a String plist type. We should really use a native plist Array type.
  • Dictionaries with string keys should also be stored as a native 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.


IssueHunt Summary

hank121314 hank121314 has been rewarded.

Backers (Total: $113.00)

Submitted pull Requests


Tips

Crash in Observation+Combine l.36

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 { ... }

Add ability to subscribe to multiple keys or any change in a given suite

Issuehunt badges

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.


IssueHunt Summary

fredyshox fredyshox has been rewarded.

Backers (Total: $64.00)

Submitted pull Requests


Tips

Support `[String: Any]`

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?

Add option to only trigger on changed value

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?

Derived keys

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.

SwiftUI support

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
}

Drop support for Carthage and Cocoapods

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.

Combine support

Issuehunt badges

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 Summary

fredyshox fredyshox has been rewarded.

Backers (Total: $100.00)

Submitted pull Requests


Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

Handle key with a dot

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?

Assigning nil to OptionalKey does not work

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.

Initializer is inaccessible due to 'internal' protection level

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)
}

Reproducible crash of Defaults.publisher()

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.

CocoaPods support

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? 🙂

Improve serialization

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


// Some codable values like URL and enum are encoded as a top-level
// string which JSON can't handle, so we need to wrap it in an array
// We need this: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750
let data = try JSONEncoder().encode([value])
let string = String(data: data, encoding: .utf8)?.dropFirst().dropLast()
set(string, forKey: key)


Radar: https://openradar.appspot.com/radar?id=4960629655863296

Add `Defaults.updatesAll()` to only listen to the keys registered in `Defaults.Keys`

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.

Publish new release

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).

Support for iOS

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 🙌

Cannot use dot syntax for `Defaults.publisher(keys:)`

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)

Package Resolution Failed in Xcode 11

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.

Support migrations

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.

Add a `DefaultsSerializable` protocol to make it possible to do custom serializations/deserializations?

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.

Support initial value for optional key

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:

  1. Force passing a suiteName parameter too if you want to use initialValue with a non-standard suite. (and use CFPreferencesCopyValue)
  2. Keep track of keys that are set in an internal UserDefaults entry.
  3. Other ideas?

Deprecate `.observe()`

.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.

Cannot find 'key' in scope

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

Schermata 2020-12-29 alle 13 24 38

Cannot publish to Cocoapods

~/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.

Infer the string key name

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.