Skip to content

Instantly share code, notes, and snippets.

@kylpo
Created April 22, 2020 17:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kylpo/915b3f5d73b7d656c0968f18724f95e1 to your computer and use it in GitHub Desktop.
Save kylpo/915b3f5d73b7d656c0968f18724f95e1 to your computer and use it in GitHub Desktop.
@userdefault Property Wrapper
import Foundation
import Combine
enum TimeFrameOption: String, CaseIterable {
case today
case week
case month
case year
case total
}
enum ThemeOption: String, CaseIterable {
case light
case dark
case black
}
enum StartingDayOfWeekOption: String, CaseIterable {
case saturday
case sunday
case monday
}
private let themeKey = "theme"
private let timeFrameKey = "timeFrame"
private let startingDayOfWeekKey = "startingDayOfWeek"
final class Settings: ObservableObject {
static let defaultTheme = ThemeOption.light
static let defaultTimeFrame = TimeFrameOption.total
static let defaultStartingDayOfWeek = StartingDayOfWeekOption.sunday
// this is needed to conform to Subject and allow subscribe()
let objectWillChange = PassthroughSubject<Void, Never>()
private var didChangeCancellable: AnyCancellable?
private var storage: UserDefaults = .standard
// Commenting @UserDefault usage, because passing in `storage` does not work.
// This means it isn't testable :(
//
// @UserDefault(key: timeFrameKey, defaultValue: defaultTimeFrame)
// var timeFrame: TimeFrameOption
//
// Also note that the current approach of just using functions is totally fine, AND testable.
var theme: ThemeOption {
get { return getEnumValue(forKey: themeKey, defaultValue: Settings.defaultTheme) }
set { setEnumValue(newValue, forKey: themeKey) }
}
var timeFrame: TimeFrameOption {
get { return getEnumValue(forKey: timeFrameKey, defaultValue: Settings.defaultTimeFrame) }
set { setEnumValue(newValue, forKey: timeFrameKey) }
}
var startingDayOfWeek: StartingDayOfWeekOption {
get { return getEnumValue(forKey: startingDayOfWeekKey, defaultValue: Settings.defaultStartingDayOfWeek) }
set { setEnumValue(newValue, forKey: startingDayOfWeekKey)}
}
init(storage: UserDefaults = .standard) {
self.storage = storage
// from https://swiftwithmajid.com/2019/09/04/modeling-app-state-using-store-objects-in-swiftui/
didChangeCancellable = NotificationCenter.default
.publisher(for: UserDefaults.didChangeNotification)
.map { _ in () } // clear data since it isn't needed/used
.receive(on: DispatchQueue.main)
.subscribe(objectWillChange) // ping subscribers to get new values
}
private func setEnumValue<T: RawRepresentable>(_ newValue: T, forKey key: String) where T.RawValue == String {
self.objectWillChange.send()
storage.set(newValue.rawValue, forKey: themeKey)
}
private func getEnumValue<T: RawRepresentable>(forKey key: String, defaultValue: T) -> T where T.RawValue == String {
if let string = storage.string(forKey: themeKey) {
return T(rawValue: string) ?? defaultValue
}
return defaultValue
}
private func getValue<T>(forKey key: String, defaultValue: T) -> T {
return storage.object(forKey: key) as? T ?? defaultValue
}
private func setValue<T>(_ newValue: T, forKey key: String) {
self.objectWillChange.send()
storage.set(newValue, forKey: key)
}
}
import XCTest
import Combine
@testable import // insert_yout_module_name
final class TestUserDefaultPropertyWrapper: XCTestCase {
func test_UserDefault_property_wrapper_default() {
let mockSettings = MockSettings()
XCTAssertEqual(MockSettings.value, mockSettings.testSetting)
}
func test_UserDefault_property_wrapper_setter() {
// given
let mockSettings = MockSettings()
XCTAssertEqual(MockSettings.value, mockSettings.testSetting)
// when
let newSettingsValue = "beep beep, we're the sheep"
mockSettings.testSetting = newSettingsValue
// then
XCTAssertEqual(newSettingsValue, mockSettings.testSetting)
}
}
private final class MockSettings {
static let value = "value"
@UserDefault(key: "key", defaultValue: MockSettings.value, storage: UserDefaults(suiteName: #file)!)
var testSetting: String
init() {
UserDefaults(suiteName: #file)?.removePersistentDomain(forName: #file)
}
}
import Foundation
/**
```
@UserDefault(key: "key", defaultValue: "value")
var theme: String
```
*/
@propertyWrapper
struct UserDefault<T> {
var key: String
var defaultValue: T
var storage: UserDefaults = .standard
var wrappedValue: T {
set { storage.set(newValue, forKey: key) }
get { storage.object(forKey: key) as? T ?? defaultValue }
}
}
//
//Commenting out because I haven't tested/used this.
//From https://github.com/Dimillian/ACHNBrowserUI/blob/411aacbde39efd4d734bd514291b5ea6f9810048/ACHNBrowserUI/ACHNBrowserUI/wrappers/UserDefaultWrapper.swift#L32
//
//
//@propertyWrapper
//public struct UserDefaultEnum<T: RawRepresentable> where T.RawValue == String {
// let key: String
// let defaultValue: T
//
// init(_ key: String, defaultValue: T) {
// self.key = key
// self.defaultValue = defaultValue
// }
//
// public var wrappedValue: T {
// get {
// if let string = UserDefaults.standard.string(forKey: key) {
// return T(rawValue: string) ?? defaultValue
// }
// return defaultValue
// }
// set {
// UserDefaults.standard.set(newValue.rawValue, forKey: key)
// }
// }
//}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment