Swifty and modern UserDefaults

Overview

Defaults

Swifty and modern UserDefaults

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

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

It's used in production by apps like Gifski, Dato, Lungo, Battery Indicator, and HEIC Converter.

For a real-world example, see the Plash app.

Highlights

  • Strongly typed: You declare the type and default value upfront.
  • Codable support: You can store any Codable value, like an enum.
  • NSSecureCoding support: You can store any NSSecureCoding value.
  • SwiftUI: Property wrapper that updates the view when the UserDefaults value changes.
  • Publishers: Combine publishers built-in.
  • 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.

Compatibility

  • macOS 10.13+
  • iOS 12+
  • tvOS 12+
  • watchOS 5+




Migration Guides

From v4 to v5

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
  • NSColor (macOS)
  • UIColor (iOS)
  • Codable
  • NSSecureCoding

Defaults also support the above types wrapped in Array, Set, Dictionary, 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

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

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


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

Requires at least macOS 11, iOS 14, tvOS 14, watchOS 7.

Observe changes to a key

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

let observer = Defaults.observe(.isUnicornMode) { change in
	// Initial event
	print(change.oldValue)
	//=> false
	print(change.newValue)
	//=> false

	// First actual event
	print(change.oldValue)
	//=> false
	print(change.newValue)
	//=> true
}

Defaults[.isUnicornMode] = true

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

There is also an observation API using the Combine framework, exposing a Publisher for key changes:

let publisher = Defaults.publisher(.isUnicornMode)

let cancellable = publisher.sink { change in
	// Initial event
	print(change.oldValue)
	//=> false
	print(change.newValue)
	//=> false

	// First actual event
	print(change.oldValue)
	//=> false
	print(change.newValue)
	//=> true
}

Defaults[.isUnicornMode] = true

// To invalidate the observation.
cancellable.cancel()

Invalidate observations automatically

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

final class Foo {
	init() {
		Defaults.observe(.isUnicornMode) { change in
			print(change.oldValue)
			print(change.newValue)
		}.tieToLifetime(of: self)
	}
}

Defaults[.isUnicornMode] = true

The observation will be valid until self is deinitialized.

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

API

Defaults

Defaults.Keys

Type: class

Stores the keys.

Defaults.Key (alias Defaults.Keys.Key)

Defaults.Key<T>(_ key: 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.observe

Defaults.observe<T: Codable>(
	_ key: Defaults.Key<T>,
	options: ObservationOptions = [.initial],
	handler: @escaping (KeyChange<T>) -> Void
) -> Defaults.Observation

Type: func

Observe changes to a key or an optional key.

By default, it will also trigger an initial event on creation. This can be useful for setting default values on controls. You can override this behavior with the options argument.

Defaults.observe(keys: keys..., options:)

Type: func

Observe multiple keys of any type, but without any information about the changes.

Options are the same as in .observe(…) for a single key.

Defaults.publisher(_ key:, options:)

Defaults.publisher<T: Codable>(
	_ key: Defaults.Key<T>,
	options: ObservationOptions = [.initial]
) -> AnyPublisher<KeyChange<T>, Never>

Type: func

Observation API using Publisher from the Combine framework.

Available on macOS 10.15+, iOS 13.0+, tvOS 13.0+, and watchOS 6.0+.

Defaults.publisher(keys: keys…, options:)

Type: func

Combine observation API for multiple key observation, but without specific information about changes.

Available on macOS 10.15+, iOS 13.0+, tvOS 13.0+, and watchOS 6.0+.

Defaults.removeAll

Defaults.removeAll(suite: UserDefaults = .standard)

Type: func

Remove all entries from the given UserDefaults suite.

Defaults.Observation

Type: protocol

Represents an observation of a defaults key.

Defaults.Observation#invalidate

Defaults.Observation#invalidate()

Type: func

Invalidate the observation.

Defaults.Observation#tieToLifetime

@discardableResult
Defaults.Observation#tieToLifetime(of weaklyHeldObject: AnyObject) -> Self

Type: func

Keep the observation alive for as long as, and no longer than, another object exists.

When weaklyHeldObject is deinitialized, the observation is invalidated automatically.

Defaults.Observation.removeLifetimeTie

Defaults.Observation#removeLifetimeTie()

Type: func

Break the lifetime tie created by tieToLifetime(of:), if one exists.

The effects of any call to tieToLifetime(of:) are reversed. Note however that if the tied-to object has already died, then the observation is already invalid and this method has no logical effect.

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.

Defaults.migrate(keys..., to: Version)

Defaults.migrate<T: Defaults.Serializable & Codable>(keys..., to: Version)
Defaults.migrate<T: Defaults.NativeType>(keys..., to: Version)

Type: func

Migrate the given keys to the specific version.

@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 = value else {
			return nil
		}

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

	public func deserialize(_ object: Serializable?) -> Value? {
		guard
			let object = 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

Related

  • Preferences - Add a preferences window to your macOS app
  • KeyboardShortcuts - Add user-customizable global keyboard shortcuts to your macOS app
  • LaunchAtLogin - Add "Launch at Login" functionality to your macOS app
  • Regex - Swifty regular expressions
  • DockProgress - Show progress in your app's Dock icon
  • Gifski - Convert videos to high-quality GIFs on your Mac
  • More…
Comments
  • Add a `Defaults.Serializable` protocol

    Add a `Defaults.Serializable` protocol

    Summary

    Fixes #52 .

    This pr intent to let user do custom serializations and deserializations. And there are some additional features.

    Features

    1. Since UserDefaults support store array and dictionary natively. This pr add dictionary and array to isNativelySupportedType, so we can use array and dictionary natively.

    2. Add DefaultsRawRepresentableBridge to store enum, no need to confirm to Codable

    // Before 
    enum FixtureEnum: String, Codable {
    	case tenMinutes = "10 Minutes"
    	case halfHour = "30 Minutes"
    	case oneHour = "1 Hour"
    }
    
    // After
    enum FixtureEnum: String, DefaultsSerializable {
    	case tenMinutes = "10 Minutes"
    	case halfHour = "30 Minutes"
    	case oneHour = "1 Hour"
    }
    
    1. For those using custom serializations and deserializations, we also support use the type in array or dictionary type.
    enum FixtureIntEnum: Int, DefaultsSerializable {
    	case tenMinutes = 10
    	case halfHour = 30
    	case oneHour = 60
    }
    
    let key = Defaults.Key<[FixtureEnum]>("array_enum", default: [.tenMinutes, .halfHour])
    

    Breaking Changes

    But these are some breaking change:

    1. Before this pr, Value of Key need to confirm to protocol Codable. After this pr, Value of Key need to confirm to protocol DefaultsSerializable.
    // Before
    struct Unicorn: Codable {
    	var isUnicorn: Bool
    }
    
    // After
    struct Unicorn: Codable, DefaultsSerializable {
    	var isUnicorn: Bool
    }
    

    Please feel free to critique and thanks for your code review!


    IssueHunt Summary

    Referenced issues

    This pull request has been submitted to:


    opened by hank121314 71
  • Add `Defaults.AnySerializable`

    Add `Defaults.AnySerializable`

    Summary

    Defaults.AnySerializable is a type erasure for Defaults.Serializable. It can accept the value conforms to Defaults.Serializable.

    Features

    With Defaults.AnySerializable user can use Defaults.Key dynamically. User will not need to specialize the type of Defaults.Key, just use Defaults.Key<Defaults.AnySerializable>.

    Usage

    let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: 121_314)
    let int = Defaults[any].get(Int.self)
    Defaults[any].set(value: "🦄")
    print(Defaults[any].get(String.self)) //=> "🦄"
    
    opened by hank121314 18
  • Add support for NSSecureCoding

    Add support for NSSecureCoding

    This fixes #16

    I didn't want to make any breaking changes so I just added NSSecureCodingKey and NSSecureCodingOptionalKey which can be used just like Key and OptionalKey.

    Observing them also works just like with the Codables.

    I added tests for all of the new methods according to the other tests.


    IssueHunt Summary

    Referenced issues

    This pull request has been submitted to:


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

    opened by koraykoska 18
  • Xcode 13 archive error

    Xcode 13 archive error

    Hey, thanks for creating and maintaining this awesome package. Using the GM version of Xcode 13 (13A233), I can compile and run my app against the iOS 15 SDK just fine, but encountering errors when archiving the app. See screenshot below for detail.

    It seems rather strange... Maybe we need to do canImport(Combine) && canImport(SwiftUI) at the top of SwiftUI.swift? I got no idea

    Screen Shot 2021-09-14 at 10 39 42 PM
    opened by automactic 16
  • Cannot conform to protocol on Xcode 13.3 beta - macOS 12.3

    Cannot conform to protocol on Xcode 13.3 beta - macOS 12.3

    Yesterday I updated Xcode to version 13.3, and now there are a number of errors about DefaultsSerializable. When creating a new project and adding the example code from here, I get the same error. image

    opened by Wouter01 15
  • Add ability to subscribe to multiple keys and to prevent propagation

    Add ability to subscribe to multiple keys and to prevent propagation

    Attempt to fix #30

    Added CompositeUserDefaultsKeyObservation for observing multiple keys, with preventPropagation option to prevent infinite recursion when change is made within handler.

    I store flag in threadDictionary of the current thread to determine when changes should be propagated. This enables to still support changes made on other threads, while initial one is still processing handler (more in testObservePreventPropagationMultiThread test case).

    Code is still a little messy, but let me know what you think :)


    IssueHunt Summary

    Referenced issues

    This pull request has been submitted to:


    opened by fredyshox 14
  • Handle key with a dot

    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?

    bug help wanted 
    opened by kmalyshev 10
  • Improve stored representation

    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

    enhancement help wanted :gift: Rewarded on Issuehunt 
    opened by sindresorhus 10
  • Combine support

    Combine support

    Fixes #23

    Created custom Publisher and Subscription, which are using UserDefaultsKeyObservation for key observation under the hood. Publisher is publishing key changes using KeyChange<T> objects, so the output is the same to standard observation API.

    As these are Combine objects, all operators and subscribers should work just fine.

    Also I've added few tests for all KeyChange types and updated docs in readme.


    IssueHunt Summary

    Referenced issues

    This pull request has been submitted to:


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

    opened by fredyshox 9
  • Add `DefaultsObsevation.tieToLifetime(of:)`

    Add `DefaultsObsevation.tieToLifetime(of:)`

    Strongly associates the observation with the passed-in object. It then lives for at least as long as the object, and automatically invalidates either a) once no more references to it exist, or b) when it receives a notification and realizes that the object to which it was tied has died.

    opened by ThatsJustCheesy 9
  • Color space is not preserved for `Color` type on iOS

    Color space is not preserved for `Color` type on iOS

    It is however preserved for UIColor.

    Report: https://twitter.com/jordibruin/status/1516728958695354369

    I have added some tests and this one fails on iOS: https://github.com/sindresorhus/Defaults/commit/3535f3d088113cf24705014eec6a17f0fd73237f#diff-d780be73215173c5c7564ee04feb252d30d24c3842b4d9f4aada400be8f37352R21

    This seems to be a bug in when SwiftUI translates between UIColor and Color?

    bug 
    opened by sindresorhus 7
  • Version 8

    Version 8

    Some ideas for version 8. It will not happen for a long time.

    • [ ] Deprecate .observe() and .publisher(): https://github.com/sindresorhus/Defaults/issues/71
    • [ ] Remove migration code: https://github.com/sindresorhus/Defaults/issues/72
    • [ ] https://github.com/sindresorhus/Defaults/issues/120
    opened by sindresorhus 0
  • Prevent using the initializer with `default` parameter for optional values

    Prevent using the initializer with `default` parameter for optional values

    There's no way to prevent it today, but maybe when Swift supports macros or constant expressions.

    I have seen many users do:

    static let interval = Key<Double?>("interval", default: nil)
    

    The default parameter is moot in this case.

    Or even worse:

    static let interval = Key<Double?>("interval", default: 4)
    

    Which has the incorrect behavior by never allowing the value to be set to nil. (The correct solution to this is #54)

    opened by sindresorhus 0
  • Validate the default key name

    Validate the default key name

    In the next major version, we should strictly validate the key name to ensure users don't accidentally make the key's unable to be observed.

    The rules are as follows:

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

    opened by sindresorhus 3
  • Setting optional values to nil doesn't work

    Setting optional values to nil doesn't work

    extension Defaults.Keys {
      static let foo = Key<Int?>("foo", default: 1)
    }
    

    Here I have some option that defaults to 1, but is optional.

    Defaults[.foo] // => 1
    Defaults[.foo] = 2
    Defaults[.foo] // => 2
    Defaults[.foo] = nil
    Defaults[.foo] // => 1
    

    Why would explicitly setting an optional to nil make it reset to the default? Isn't that what reset() is for? Logically it would make sense to have an option that has a default value, but can also be turned off.

    I tried with an Enum:

    enum OptionalKey: Defaults.Serializable {
      case some(Int)
      case none
    }
    

    But this fails as it doesn't conform to Defaults.Serializable.

    Is there no way to have both a default key, but also retain the ability to set it to nil?

    opened by pejrich 1
  • Simplify generics when targeting Swift 5.7?

    Simplify generics when targeting Swift 5.7?

    There are some new generics features in Swift 5.7. When it's out, we should look into whether there are any things we could simplify with it or something we should adopt.

    https://github.com/apple/swift/blob/main/CHANGELOG.md#swift-57

    opened by sindresorhus 0
  • Support `CodingKeyRepresentable` for `DictionaryBridge`?

    Support `CodingKeyRepresentable` for `DictionaryBridge`?

    https://github.com/apple/swift-evolution/blob/main/proposals/0320-codingkeyrepresentable.md

    It requires macOS 12.3 / iOS 15.4 so we would have to preserve the existing bridge.

    opened by sindresorhus 0
Releases(v7.0.0)
  • v7.0.0(Nov 19, 2022)

    Breaking

    • Target macOS 10.15, iOS 13, tvOS 13, watchOS 6 https://github.com/sindresorhus/Defaults/commit/ea11b7ac4f5d006fe46d00b428631a064445990d
    • Require Xcode 14.1

    Improvements

    • Add .updates() method to observe updates to values https://github.com/sindresorhus/Defaults/commit/7a22d378742acbef49ecff11b9de8bf8b72b34dc
      • I recommend moving to this method from .observe() and .publisher(). While these methods will remain for a while, they will eventually be deprecated in favor of .updates().
    • Support dynamic default value https://github.com/sindresorhus/Defaults/commit/fbc67fd179b6c7498a4c9ecd6d99a2c75b30a4fa
    • Support serializing and deserializing nested custom types https://github.com/sindresorhus/Defaults/commit/be7e30ba366a7a10e198671505dbe6b4e07b76bc

    https://github.com/sindresorhus/Defaults/compare/v6.3.0...v7.0.0

    Source code(tar.gz)
    Source code(zip)
  • v6.3.0(Jun 10, 2022)

    • Add support for ClosedRange and Range types https://github.com/sindresorhus/Defaults/commit/bab3087067ae08cd20e629405f2b85a575783215
    • Make Defaults.AnyKey (including Defaults.Key) conform to Equatable and Hashable https://github.com/sindresorhus/Defaults/commit/001adc694b78005baf3a42a715b98f7f5610f650
    • Fix preserving color space for the Color type https://github.com/sindresorhus/Defaults/commit/9e65eac602fe7d786275a7b3f0714474273e1781

    https://github.com/sindresorhus/Defaults/compare/v6.2.1...v6.3.0

    Source code(tar.gz)
    Source code(zip)
  • v6.2.1(Mar 15, 2022)

    The Swift version included in Xcode 13.3 has a bug that affects Defaults. If you get a compile error, see the workaround. Make sure you also upgrade to Defaults v6.2.1 (this version).

    https://github.com/sindresorhus/Defaults/compare/v6.2.0...v6.2.1

    Source code(tar.gz)
    Source code(zip)
  • v6.2.0(Jan 27, 2022)

  • v6.1.0(Oct 16, 2021)

    • Add support for SwiftUI Color (#84) https://github.com/sindresorhus/Defaults/commit/55f3302c3ab30a8760f10042d0ebc0a6907f865a

    https://github.com/sindresorhus/Defaults/compare/v6.0.0...v6.1.0

    Source code(tar.gz)
    Source code(zip)
  • v6.0.0(Oct 12, 2021)

    Breaking

    • New platform requirements:
      • macOS 10.12 → 10.13
      • iOS 10 → 12
      • tvOS 10 → 12
      • watchOS 3 → 5

    Improvements

    Fixes

    • Fix archive error with Xcode 13 https://github.com/sindresorhus/Defaults/issues/81

    https://github.com/sindresorhus/Defaults/compare/v5.0.0...v6.0.0

    Source code(tar.gz)
    Source code(zip)
  • v5.0.0(Jun 1, 2021)

    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.

    Meta

    Huge thanks to @hank121314 for doing a lot of the work on this release.


    https://github.com/sindresorhus/Defaults/compare/v4.2.2...v5.0.0

    Source code(tar.gz)
    Source code(zip)
  • v5.0.0-beta.1(May 18, 2021)

    Please help us try out this beta release. And let us know if something in the migration guide could be improved.

    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.

    https://github.com/sindresorhus/Defaults/compare/v4.2.2...v5.0.0-beta.1

    Source code(tar.gz)
    Source code(zip)
  • v4.2.2(Apr 18, 2021)

    • Fix crash in .publisher() caused by immediately cancelling subscriber (#66) https://github.com/sindresorhus/Defaults/commit/6158b9bdb3820ab896b2323f37f5cd3b24ea8b27

    https://github.com/sindresorhus/Defaults/compare/v4.2.1...v4.2.2

    Source code(tar.gz)
    Source code(zip)
  • v4.2.1(Feb 26, 2021)

    • Fix regression in 4.2.0 regarding availability annotations https://github.com/sindresorhus/Defaults/commit/c956886bba053c40d9915891812b5620526409f9
    Source code(tar.gz)
    Source code(zip)
  • v4.2.0(Feb 20, 2021)

    • Support .removeDuplicates() for Defaults.publisher (#60) https://github.com/sindresorhus/Defaults/commit/760dbfb0777610be90844c4efab5aa420003bd16
    Source code(tar.gz)
    Source code(zip)
  • v4.1.0(Aug 28, 2020)

    • Add ability to subscribe to multiple keys https://github.com/sindresorhus/Defaults/commit/ab8127604c7f8b5ed9b6369e4d1ec5fb1b39b971
    • Add ability to prevent event propagation https://github.com/sindresorhus/Defaults/commit/ab8127604c7f8b5ed9b6369e4d1ec5fb1b39b971

    https://github.com/sindresorhus/Defaults/compare/v4.0.0...v4.1.0

    Source code(tar.gz)
    Source code(zip)
  • v4.0.0(Apr 18, 2020)

    Important

    If you use Swift Package Manager, you need to set the build setting “Other Linker Flags” to -weak_framework Combine to work around this Xcode bug.

    Breaking

    • Get rid of Defaults.OptionalKey https://github.com/sindresorhus/Defaults/commit/b2fdee2055c87149bef0b2fec0726a87c191926c
      • Migrate:
     extension Defaults.Keys {
    -	static let name = OptionalKey<Double>("name")
    +	static let name = Key<Double?>("name")
     }
    
    • Remove the .old and .new options for Defaults.observe https://github.com/sindresorhus/Defaults/commit/8376ca7f5157d692b010e731878a82b49ba1c70e
      • They're now the default. There was no good reason to not specify them and it was easy to leave them out by accident and then getting the incorrect .newValue/.oldValue.
    • Rename DefaultsObservation to Defaults.Observation https://github.com/sindresorhus/Defaults/commit/31b56ce018f03b07cc335960d8f7aa1d49d9b0f8

    Improvements

    • Add @Default property wrapper for SwiftUI https://github.com/sindresorhus/Defaults/commit/12a65c057d0defa2fb500dd15ae4255fb274f16e
    • Add Combine publishers https://github.com/sindresorhus/Defaults/commit/6029ac796b7bc75edf54acc7adf42e169eadbe9b
    • .reset() now actually removes the item instead of just setting its value to nil.

    Fixes

    • Defaults.reset() now works with keys of different types, but it's limited to 10 keys because of Swift generics limitations https://github.com/sindresorhus/Defaults/commit/15c096d7fd7e7ef0ad4e4824cb3f96bad941440f

    https://github.com/sindresorhus/Defaults/compare/v3.1.1...v4.0.0

    Source code(tar.gz)
    Source code(zip)
  • v3.1.1(Oct 30, 2019)

  • v3.1.0(Oct 30, 2019)

  • v3.0.0(Sep 11, 2019)

    Breaking

    • Require Xcode 11 and Swift 5.1 for building https://github.com/sindresorhus/Defaults/commit/90ac6f88021e22d58b109b71866bd21471b898fe
    • Switch from defaults to Defaults https://github.com/sindresorhus/Defaults/commit/90ac6f88021e22d58b109b71866bd21471b898fe Example: defaults[.unicorn]Defaults[.unicorn] Example: defaults.observableDefaults.observable
    • Rename defaults.clear to Defaults.removeAll and make it a static method https://github.com/sindresorhus/Defaults/commit/27c9997134dacd097b912a39a685ea71a0a57b89

    Enhancements

    • Add .reset() method to reset the given keys back to their default value https://github.com/sindresorhus/Defaults/commit/d1e42154f9a2c9c7375849bd376b99a48deda47b
    Source code(tar.gz)
    Source code(zip)
  • v2.0.2(Jul 25, 2019)

  • v2.0.1(Jul 8, 2019)

  • v2.0.0(Apr 5, 2019)

  • v1.0.0(Oct 17, 2018)

    Key observation

    You can now observe changes to keys in a strongly-typed fashion. It even preserves the type of the oldValue and newValue keys in the change event.

    extension Defaults.Keys {
    	static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
    }
    
    let observer = defaults.observe(.isUnicornMode) { change in
    	// Initial event
    	print(change.oldValue)
    	//=> false
    	print(change.newValue)
    	//=> false
    
    	// First actual event
    	print(change.oldValue)
    	//=> false
    	print(change.newValue)
    	//=> true
    }
    
    defaults[.isUnicornMode] = true
    

    Shorter syntax

     extension Defaults.Keys {
    -	static let quality = Defaults.Key<Double>("quality", default: 0.8)
    +	static let quality = Key<Double>("quality", default: 0.8)
     }
    

    Support for alternative UserDefaults suites

    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
    

    Registers default values with the native UserDefaults

    When you use, for example:

    extension Defaults.Keys {
    	static let quality = Key<Double>("quality", default: 0.8)
    }
    

    It will register 0.8 as the default value with UserDefaults, which can then be used in other contexts, like binding in Interface Builder.

    Source code(tar.gz)
    Source code(zip)
Owner
Sindre Sorhus
Full-Time Open-Sourcerer. Focuses on Swift & JavaScript. Makes macOS apps, CLI tools, npm packages. Likes unicorns.
Sindre Sorhus
Prephirences is a Swift library that provides useful protocols and convenience methods to manage application preferences, configurations and app-state. UserDefaults

Prephirences - Preϕrences Prephirences is a Swift library that provides useful protocols and convenience methods to manage application preferences, co

Eric Marchand 557 Nov 22, 2022
Simple, Strongly Typed UserDefaults for iOS, macOS and tvOS

简体中文 DefaultsKit leverages Swift 4's powerful Codable capabilities to provide a Simple and Strongly Typed wrapper on top of UserDefaults. It uses less

Nuno Dias 1.4k Nov 20, 2022
🔍 Browse and edit UserDefaults on your app

UserDefaults-Browser Browse and edit UserDefaults on your app. (SwiftUI or UIKit) Browse Edit (as JSON) Edit (Date) Export Note: We recommend to use S

Yusuke Hosonuma 25 Nov 3, 2022
Effortlessly synchronize UserDefaults over iCloud.

Zephyr ??️ Effortlessly sync UserDefaults over iCloud About Zephyr synchronizes specific keys and/or all of your UserDefaults over iCloud using NSUbiq

Arthur Ariel Sabintsev 837 Nov 11, 2022
Why not use UserDefaults to store Codable objects 😉

tl;dr You love Swift's Codable protocol and use it everywhere, who doesn't! Here is an easy and very light way to store and retrieve -reasonable amoun

Omar Albeik 452 Oct 17, 2022
A lightweight wrapper over UserDefaults/NSUserDefaults with an additional layer of AES-256 encryption

SecureDefaults for iOS, macOS Requirements • Usage • Installation • Contributing • Acknowledgments • Contributing • Author • License SecureDefaults is

Victor Peschenkov 212 Nov 14, 2022
⚙️ A tiny property wrapper for UserDefaults. Only 60 lines of code.

⚙️ A tiny property wrapper for UserDefaults. Only 60 lines of code. import Persistent extension UserDefaults { // Optional property @Per

Mezhevikin Alexey 6 Sep 28, 2022
A Swifty API for global macOS hotkeys.

A Swifty API for global macOS hotkeys. Install Add the following dependency to your Package.swift file: .package(url: "https://github.com/jordanbaird/

Jordan Baird 7 Oct 23, 2022
A demo app to showcase pixel perfect, modern iOS development with SwiftUI and Combine on MVVM-C architecture.

Pixel_Perfect_SwiftUI A demo app to showcase pixel perfect, modern iOS development with SwiftUI and Combine on MVVM-C architecture. Tech Stack: Swift,

Burhan Aras 0 Jan 9, 2022
Modern Swift API for NSUserDefaults

SwiftyUserDefaults Modern Swift API for NSUserDefaults SwiftyUserDefaults makes user defaults enjoyable to use by combining expressive Swifty API with

Luke 4.7k Nov 16, 2022
Modern Swift API for NSUserDefaults

SwiftyUserDefaults Modern Swift API for NSUserDefaults SwiftyUserDefaults makes user defaults enjoyable to use by combining expressive Swifty API with

Luke 4.7k Nov 16, 2022
Classes-and-structures-in-swift - This source files show what is the difference between class and structure

This source files show what is the difference between class and structure You ca

null 0 Jan 4, 2022
Elegant library to manage the interactions between view and model in Swift

An assistant to manage the interactions between view and model ModelAssistant is a mediator between the view and model. This framework is tailored to

Seyed Samad Gholamzadeh 28 Jan 29, 2022
Store and retrieve Codable objects to various persistence layers, in a couple lines of code!

tl;dr You love Swift's Codable protocol and use it everywhere, who doesn't! Here is an easy and very light way to store and retrieve Codable objects t

null 148 Nov 3, 2022
🛶Shallows is a generic abstraction layer over lightweight data storage and persistence.

Shallows Shallows is a generic abstraction layer over lightweight data storage and persistence. It provides a Storage<Key, Value> type, instances of w

Oleg Dreyman 619 Sep 16, 2022
Disk is a powerful and simple file management library built with Apple's iOS Data Storage Guidelines in mind

Disk is a powerful and simple file management library built with Apple's iOS Data Storage Guidelines in mind

Saoud Rizwan 3k Nov 22, 2022
KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible.

KeyPathKit Context Swift 4 has introduced a new type called KeyPath, with allows to access the properties of an object with a very nice syntax. For in

Vincent Pradeilles 405 Nov 7, 2022
StorageManager - FileManager framework that handels Store, fetch, delete and update files in local storage

StorageManager - FileManager framework that handels Store, fetch, delete and update files in local storage. Requirements iOS 8.0+ / macOS 10.10+ / tvOS

Amr Salman 47 Nov 3, 2022
Listens to changes in a PostgreSQL Database and via websockets.

realtime-swift Listens to changes in a PostgreSQL Database and via websockets. A Swift client for Supabase Realtime server. Usage Creating a Socket co

Supabase 32 Oct 23, 2022