Skip to content

Instantly share code, notes, and snippets.

@florianpircher
Last active April 14, 2023 16:06
Show Gist options
  • Save florianpircher/4513f8def656fc4fe427345bce7b052b to your computer and use it in GitHub Desktop.
Save florianpircher/4513f8def656fc4fe427345bce7b052b to your computer and use it in GitHub Desktop.
Combine publishers for user defaults. For user defaults property wrappers, see: https://gist.github.com/florianpircher/626174760c2d91bab6defbf1b2b3d4cf
//
// User Defaults Publishers.swift
// Light Table
//
// Copyright 2021 Florian Pircher
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Cocoa
import Combine
/// The values of user defaults are located under the `values` property of `NSUserDefaultsController`.
/// This prefix is used to read user defaults keys on `NSUserDefaultsController`.
fileprivate let userDefaultsKeyPathPrefix = "values."
extension UserDefaults {
/// A ValuePublisher publishes the initial value and any new values for a user defaults key.
///
/// The `read` function reads a value from a given user defaults store.
/// The returned value is passed along to the subscribers of the publisher.
///
/// Since user defaults are string-based, they cannot be observed by the default `publisher(for:)` on `NSObject`, which uses KVO with type-safe key paths.
/// Instead, the following methods are offered on `UserDefaults`:
///
/// - `boolPublisher(forKey:)` — publishes `Bool`
/// - `integerPublisher(forKey:)` — publishes `Int`
/// - `floatPublisher(forKey:)` — publishes `Float`
/// - `doublePublisher(forKey:)` — publishes `Double`
/// - `stringPublisher(forKey:)` — publishes `String?`
/// - `urlPublisher(forKey:)` — publishes `URL?`
/// - `dataPublisher(forKey:)` — publishes `Data?`
/// - `arrayPublisher(forKey:)` — publishes `[Any]?`
/// - `stringArrayPublisher(forKey:)` — publishes `[String]?`
/// - `dictionaryPublisher(forKey:)` — publishes `[String: Any]?`
/// - `objectPublisher(forKey:)` — publishes `Any?`
struct ValuePublisher<Value>: Publisher {
typealias Output = Value
typealias Failure = Never
/// The key of the published user default.
let key: String
/// Returns the value that is passed along to the subscribers.
let read: (String) -> Output
func receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {
let subscription = ValueSubscription(target: subscriber, key: key, read: read)
subscriber.receive(subscription: subscription)
subscription.connect()
}
}
/// A ValueSubscription uses KVO to inform its subscriber about the initial and any new values.
final class ValueSubscription<Target: Subscriber>: NSObject, Subscription {
typealias Value = Target.Input
/// The subscriber; `nil` if the subscription has been canceled.
private var target: Target?
/// The key of the published user default.
let key: String
/// Returns the value that is passed along to the subscriber; `nil` if the subscription has been canceled.
private var read: ((String) -> Value)?
init(target: Target, key: String, read: @escaping (String) -> Value) {
self.target = target
self.key = key
self.read = read
super.init()
}
/// Starts the user defaults observation and sends the initial value to the subscriber.
func connect() {
NSUserDefaultsController.shared.addObserver(self, forKeyPath: userDefaultsKeyPathPrefix + key, options: [], context: nil)
}
/// Publishes the current value to the subscriber.
private func publish() {
guard
let target,
let read
else {
return
}
// user defaults are not expected to change rapidly; the demand return value can thus be discarded
let _ = target.receive(read(key))
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard
let keyPath,
keyPath.hasPrefix(userDefaultsKeyPathPrefix),
keyPath.dropFirst(userDefaultsKeyPathPrefix.count) == key,
context == nil
else {
// discard any values that are unrelated to the registered user defaults key
return
}
publish()
}
func request(_ demand: Subscribers.Demand) {
if demand.max.map({ $0 > 0 }) ?? true {
publish()
}
}
func cancel() {
NSUserDefaultsController.shared.removeObserver(self, forKeyPath: userDefaultsKeyPathPrefix + key)
target = nil
read = nil
}
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.bool(forKey:)`.
func boolPublisher(forKey defaultName: String) -> ValuePublisher<Bool> {
ValuePublisher(key: defaultName, read: bool(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.integer(forKey:)`.
func integerPublisher(forKey defaultName: String) -> ValuePublisher<Int> {
ValuePublisher(key: defaultName, read: integer(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.float(forKey:)`.
func floatPublisher(forKey defaultName: String) -> ValuePublisher<Float> {
ValuePublisher(key: defaultName, read: float(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.double(forKey:)`.
func doublePublisher(forKey defaultName: String) -> ValuePublisher<Double> {
ValuePublisher(key: defaultName, read: double(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.string(forKey:)`.
func stringPublisher(forKey defaultName: String) -> ValuePublisher<String?> {
ValuePublisher(key: defaultName, read: string(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.url(forKey:)`.
func urlPublisher(forKey defaultName: String) -> ValuePublisher<URL?> {
ValuePublisher(key: defaultName, read: url(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.data(forKey:)`.
func dataPublisher(forKey defaultName: String) -> ValuePublisher<Data?> {
ValuePublisher(key: defaultName, read: data(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.array(forKey:)`.
func arrayPublisher(forKey defaultName: String) -> ValuePublisher<[Any]?> {
ValuePublisher(key: defaultName, read: array(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.stringArray(forKey:)`.
func stringArrayPublisher(forKey defaultName: String) -> ValuePublisher<[String]?> {
ValuePublisher(key: defaultName, read: stringArray(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.dictionary(forKey:)`.
func dictionaryPublisher(forKey defaultName: String) -> ValuePublisher<[String: Any]?> {
ValuePublisher(key: defaultName, read: dictionary(forKey:))
}
/// Publishes the initial value and any new values associated with the specific key as read by `UserDefaults.object(forKey:)`.
func objectPublisher(forKey defaultName: String) -> ValuePublisher<Any?> {
ValuePublisher(key: defaultName, read: object(forKey:))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment