
Вряд ли можно представить хоть одно приложение, которое не требует сохранения настроек пользователя. Очевидно, что для этих целей существует масса решений, и каждое из них решает одну, конкретную задачу. Сегодня речь пойдет о постоянном хранилище данных UserDefaults и его использовании для хранения данных.
Реализация
Выполнив поиск существующих решений на просторах сети интернет были найдены следующие материалы:
Create the Perfect UserDefaults Wrapper Using Property Wrapper
A better approach to writing a UserDefaults Property Wrapper
Однако все они имеют несколько недостатков:
Значение не может быть опциональным, вследствие чего требуется указывать значение по-умолчанию
Значение не может быть перечислением или OptionSet
Все они позволяют сохранить в хранилище неподдерживаемые типы данных, вследствие чего приложение аварийно завершает свою работу
Но если обратиться к первоисточнику и изучить AppStorage от Apple, то единственный недостаток, присущий уже к данному решению является минимальная версия iOS 14, а также зависимость от framework SwiftUI.
Ниже предлагается propertyWrapper UserDefault, который лишен всех перечисленных недостатков, в то время как решает поставленную задачу.
import SwiftUI @propertyWrapper public struct UserDefault<Value>: DynamicProperty { private let get: () -> Value private let set: (Value) -> Void public var wrappedValue: Value { get { get() } nonmutating set { set(newValue) } } } public extension UserDefault { init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Bool { self.init(defaultValue: wrappedValue, key: key, store: store) } init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Int { self.init(defaultValue: wrappedValue, key: key, store: store) } init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Double { self.init(defaultValue: wrappedValue, key: key, store: store) } init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == String { self.init(defaultValue: wrappedValue, key: key, store: store) } init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == URL { self.init(defaultValue: wrappedValue, key: key, store: store) } init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Data { self.init(defaultValue: wrappedValue, key: key, store: store) } private init(defaultValue: Value, key: String, store: UserDefaults) { get = { let value = store.value(forKey: key) as? Value return value ?? defaultValue } set = { newValue in store.set(newValue, forKey: key) } } } public extension UserDefault where Value: ExpressibleByNilLiteral { init(_ key: String, store: UserDefaults = .standard) where Value == Bool? { self.init(wrappedType: Bool.self, key: key, store: store) } init(_ key: String, store: UserDefaults = .standard) where Value == Int? { self.init(wrappedType: Int.self, key: key, store: store) } init(_ key: String, store: UserDefaults = .standard) where Value == Double? { self.init(wrappedType: Double.self, key: key, store: store) } init(_ key: String, store: UserDefaults = .standard) where Value == String? { self.init(wrappedType: String.self, key: key, store: store) } init(_ key: String, store: UserDefaults = .standard) where Value == URL? { self.init(wrappedType: URL.self, key: key, store: store) } init(_ key: String, store: UserDefaults = .standard) where Value == Data? { self.init(wrappedType: Data.self, key: key, store: store) } private init<T>(wrappedType: T.Type, key: String, store: UserDefaults) { get = { let value = store.value(forKey: key) as? Value return value ?? nil } set = { newValue in let newValue = newValue as? Optional<T> if let newValue { store.set(newValue, forKey: key) } else { store.removeObject(forKey: key) } } } } public extension UserDefault where Value: RawRepresentable { init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == String { self.init(defaultValue: wrappedValue, key: key, store: store) } init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == Int { self.init(defaultValue: wrappedValue, key: key, store: store) } private init(defaultValue: Value, key: String, store: UserDefaults) { get = { var value: Value? if let rawValue = store.value(forKey: key) as? Value.RawValue { value = Value(rawValue: rawValue) } return value ?? defaultValue } set = { newValue in let value = newValue.rawValue store.set(value, forKey: key) } } } public extension UserDefault { init<R>(_ key: String, store: UserDefaults = .standard) where Value == R?, R: RawRepresentable, R.RawValue == Int { self.init(key: key, store: store) } init<R>(_ key: String, store: UserDefaults = .standard) where Value == R?, R: RawRepresentable, R.RawValue == String { self.init(key: key, store: store) } private init<R>(key: String, store: UserDefaults) where Value == R?, R: RawRepresentable { get = { if let rawValue = store.value(forKey: key) as? R.RawValue { return R(rawValue: rawValue) } else { return nil } } set = { newValue in let newValue = newValue as Optional<R> if let newValue { store.set(newValue.rawValue, forKey: key) } else { store.removeObject(forKey: key) } } } }
Стоит отметить, что использование propertyWrapper намного удобней, так как кода становится существенно меньше, соответственно и читается он легче.
final class UserSettings { @UserDefault("age") var age: Int? @UserDefault("name") var name: String? @UserDefault("planet") var planet: Planet = .earth func clear() { age = nil name = nil planet = .earth } }
Заключение
В последнее время, с момента появления SwiftUI, все большей популярностью пользуются @propertyWrapper. Однако использование таких объектов накладывает дополнительные ограничения. К примеру, необходимо поднимать минимальную версию iOS и внедрять дополнительную зависимость от framework SwiftUI. Но это вовсе не означает, что нужно пользоваться тем, что есть, и пример тому был описан в этой статье.
