Skip to content

Instantly share code, notes, and snippets.

@skyofdwarf
Last active December 14, 2022 12:55
Show Gist options
  • Save skyofdwarf/d1e0c63a07a6979a037aca26e4a9d4ab to your computer and use it in GitHub Desktop.
Save skyofdwarf/d1e0c63a07a6979a037aca26e4a9d4ab to your computer and use it in GitHub Desktop.
PropertyWrapper 를 이용한 UserDefaults
//
// UserDefaultManager.swift
// UserDefault
//
// Created by YEONGJUNG KIM on 2022/12/14.
//
import Foundation
/// UserDefaults property wrapper
///
/// Optional, Boolean, Int, Float, Array, Dictionary 타입의 경우 명시적으로 기본값을 지정하지 않아도 사용가능하다.
/// 그 외 타입은 초기값이 지정되지 않으면 기본값을 명시해야 한다.
/// 초기값의 경우 UserDefaults에 값이 추가되기 때문에, 초기값이 필요한지 기본값(UserDefaults에 값이 없는 경우 리턴할)이 필요한지
/// 판단하고 사용해야 한다.
///
/// ```swift
/// // initial value without default value
/// @UserDefaulted("key1")
/// var string: String = "initial string value"
///
/// // no initial value with default value
/// @UserDefaulted("key3", default: "default value")
/// var defaultString: String
///
/// // optional type with nil default value and without initial value
/// @UserDefaulted(key: "key2")
/// var ostring: String?
/// ```
///
/// ```
/// enum DiaryKey: String, CustomStringConvertible {
/// case date, tag
/// var description: String { rawValue }
/// }
///
/// @UserDefaulted(key: DiaryKey.tag)
/// var tag: String?
/// ```
///
/// - Note: 초기값과 기본값을 구별하여 사용하자
/// 초기값을 지정하면 wrappedValue setter가 호출되어 데이터가 저장되므로
/// 비휘발성 데이터의 경우 디스크/네트워크 등에서 읽어오기 전에 값을 덮어쓰일 수 있으니 주의하자
///
/// - Note: 초기값(wrappedValue)이 지정되지 않은 propertyWrapper의 경우, memberwise initializer(구조체 컴파일러 자동생성 초기화)에서
/// T 가 아닌 Defaulted<T>로 생성된다.
@propertyWrapper
struct UserDefaulted<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
guard let object = UserDefaults.standard.object(forKey: key), /* 실제 데이터가 존재하는지 */
let value = object as? T /* 데이터가 T타입인지 */
else {
return defaultValue
}
return value
}
set {
if newValue is ExpressibleByNilLiteral {
let mirror = Mirror(reflecting: newValue)
if mirror.displayStyle == .optional, mirror.children.isEmpty {
UserDefaults.standard.removeObject(forKey: key)
} else {
UserDefaults.standard.set(newValue, forKey: key)
}
} else {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
/// 주어진 초기값을 UserDefaults에 추가하고 기본값으로도 사용한다.
///
/// - Note: 생성 시 UserDefaults에 초기값 추가를 원하는 것이 아니라면 다른 생성자(`init(key:default:)`)를 이용하자.
init(wrappedValue: T, key: @autoclosure () -> CustomStringConvertible) {
self.key = key().description
self.defaultValue = wrappedValue
self.wrappedValue = wrappedValue
}
/// 주어진 초기값과 기본값을 사용하여 UserDefaults을 생성한다. 초기값은 UserDefaults에 바로 추가된다.
///
/// - Note: 생성 시 UserDefaults에 초기값 추가를 원하는 것이 아니라면 다른 생성자(`init(key:default:)`)를 이용하자.
init(wrappedValue: T, key: @autoclosure () -> CustomStringConvertible, default defaultValue: T) {
self.key = key().description
self.defaultValue = defaultValue
self.wrappedValue = wrappedValue
}
/// UserDefaults에 키값 추가없이 프로터피를 생성한다, 조회 시 값이 없는 경우 리턴할 기본값을 명시한다.
init(key: @autoclosure () -> CustomStringConvertible, default defaultValue: T) {
self.key = key().description
self.defaultValue = defaultValue
}
}
// MARK: 몇 원형 타입들에 대한 기본값 지원
extension UserDefaulted where T: ExpressibleByNilLiteral {
/// Optional 타입 기본값 지원
init(key: @autoclosure () -> CustomStringConvertible, default defaultValue: T = nil) {
self.key = key().description
self.defaultValue = defaultValue
}
}
extension UserDefaulted where T: ExpressibleByBooleanLiteral {
/// Boolean 타입 기본값 지원
init(key: @autoclosure () -> CustomStringConvertible, default defaultValue: T = false) {
self.key = key().description
self.defaultValue = defaultValue
}
}
extension UserDefaulted where T: BinaryFloatingPoint/*ExpressibleByFloatLiteral*/ {
/// Float 타입 기본값 지원
///
/// ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral 사용 시,
/// `Ambiguous use of 'init(key:default:)'` 에러를 방지하기 위해 `BinaryFloatingPoint`를 대신 사용한다.
init(key: @autoclosure () -> CustomStringConvertible, default defaultValue: T = 0.0) {
self.key = key().description
self.defaultValue = defaultValue
}
}
extension UserDefaulted where T: ExpressibleByIntegerLiteral {
/// Integer 타입 기본값 지원
init(key: @autoclosure () -> CustomStringConvertible, default defaultValue: T = 0) {
self.key = key().description
self.defaultValue = defaultValue
}
}
extension UserDefaulted where T: ExpressibleByArrayLiteral {
/// Aray 타입 기본값 지원
init(key: @autoclosure () -> CustomStringConvertible, default defaultValue: T = []) {
self.key = key().description
self.defaultValue = defaultValue
}
}
extension UserDefaulted where T: ExpressibleByDictionaryLiteral, T.Key == String {
/// [String: Any] 타입 기본값 지원
init(key: @autoclosure () -> CustomStringConvertible, default defaultValue: T = [:]) {
self.key = key().description
self.defaultValue = defaultValue
}
}
// MARK: CustomStringConvertible
extension UserDefaulted: CustomStringConvertible {
var description: String {
"\(key): '\(wrappedValue)'"
}
}
@skyofdwarf
Copy link
Author

enum EnumKeys: String, CustomStringConvertible {
    case one
    case two
    
    var description: String { rawValue }
}

struct Defaults {
    @UserDefaulted(key: EnumKeys.one)
    var enum_key: Int = 0
    
    // normal type
    @UserDefaulted(key: "string_with_initial_value")
    var string_with_initial_value: String = "initial value"
    
    @UserDefaulted(key: "string_with_default_value", default: "default")
    var string_with_default_value: String
    
    @UserDefaulted(key: "bool_with_initial_value_true")
    var bool_with_initial_value_true: Bool = true
    
    @UserDefaulted(key: "int_with_initial_value_2")
    var int_with_initial_value_2: Int = 2
    
    @UserDefaulted(key: "double_with_initial_value_1dot23")
    var double_with_initial_value_1dot23: Double = 1.23
    
    // optional type
    @UserDefaulted(key: "optional_with_default_value_nil")
    var optional_with_default_value_nil: String?
    
    @UserDefaulted(key: "optional_with_initial_value")
    var optional_with_initial_value: String? = "initial value"
    
    @UserDefaulted(key: "optional_with_default_value", default: "default")
    var optional_with_default_value: String?

    // explicit default value
    @UserDefaulted(key: "bool_with_default_value_true", default: true)
    var bool_with_explicit_default_value_true: Bool
    
    @UserDefaulted(key: "int_with_default_value_333", default: 333)
    var int_with_explicit_default_value_333: Int
    
    @UserDefaulted(key: "double_with_default_value_1dot2", default: 1.2)
    var double_with_explicit_default_value_1dot2: Double
    
    @UserDefaulted(key: "dict_with_explicit_default_value", default: [:])
    var dict_with_explicit_default_value: [String: Int]

    // implict default value
    @UserDefaulted(key: "bool_with_implicit_default_value_false")
    var bool_with_implicit_default_value_false: Bool

    // explict default value
    @UserDefaulted(key: "int_with_implicit_default_value_0")
    var int_with_implicit_default_value_0: Int
    
    @UserDefaulted(key: "double_with_implicit_default_value_0")
    var double_with_implicit_default_value_0: Double
    
    @UserDefaulted(key: "dict_with_implicit_default_value")
    var dict_with_implicit_default_value: [String: Any]
    
    @UserDefaulted(key: "array_with_implicit_default_value")
    var array_with_implicit_default_value: [Int]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment