
Вряд ли можно представить хоть одно приложение, которое не требует сохранения настроек пользователя. Очевидно, что для этих целей существует масса решений, и каждое из них решает одну, конкретную задачу. Сегодня речь пойдет о постоянном хранилище данных UserDefaults и его использовании для хранения данных.
Интерфейс хранилища
При работе с UserDefaults разработчика чаще всего интересуют три метода:
func set(_ value: Any?, forKey defaultName: String)
func removeObject(forKey defaultName: String)
func object(forKey defaultName: String) -> Any?Поскольку этот компонент был представлен широкой публике еще с выходом iOS 2.0, интерфейс с полной уверенностью можно назвать устаревшим.
Для вызова в качестве ключа нужно передавать строку.
Работа происходит с объектом типа Any, а не с конкретным типом.
Работа происходит с ограниченным списком типов. Даже перечисления и OptionSet вызывают crash. (Сразу сделаем оговорку, что Codable объекты в UserDefaults хранить не стоит).
Ниже предлагается решение всех этих недостатков в виде объекта UserDefaultsContainer со следующим интерфейсом:
public init(keyedBy: K.Type, userDefaults: UserDefaults = .standard)
public subscript<T>(key: K) -> T? { get set }
public subscript<T>(key: K) -> T? where T : RawRepresentable { get set }import Foundation
public final class UserDefaultsContainer<K: CodingKey> {
private let userDefaults: UserDefaults
public init(keyedBy: K.Type, userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}
public subscript<T>(key: K) -> T? {
get { getValue(forKey: key.stringValue) as? T }
set { set(value: newValue, forKey: key.stringValue) }
}
public subscript<T>(key: K) -> T? where T: RawRepresentable {
get {
if let rawValue = getValue(forKey: key.stringValue) as? T.RawValue {
return T(rawValue: rawValue)
} else {
return nil
}
}
set {
set(value: newValue?.rawValue, forKey: key.stringValue)
}
}
private func set(value: Any?, forKey key: String) {
if let value {
userDefaults.set(value, forKey: key)
} else {
userDefaults.removeObject(forKey: key)
}
}
private func getValue(forKey key: String) -> Any? {
return userDefaults.object(forKey: key)
}
}
Можно было бы назвать данный объект Adapter, ссылаясь на паттерн проектирования, но в угоду консистентности и единства с решениями от Apple, название отличается.
Самое время определить требования для работы с хранилищем:
Объект может быть опциональным, значение которого может отсутствовать.
Объект может быть не опциональным, имеющим значение по умолчанию.
Объект может быть перечислением или OptionSet.
Далее представлена структура UserSettings, которая содержит все необходимые настройки пользователя и обеспечивает все перечисленные выше требования:
struct UserSettings {
// MARK: - Types
private enum SettingsKey: CodingKey {
case age
case name
case planet
}
// MARK: - Properties
private let storage = UserDefaultsContainer(keyedBy: SettingsKey.self)
var age: Int? {
get { storage[.age] }
set { storage[.age] = newValue }
}
var name: String? {
get { storage[.name] }
set { storage[.name] = newValue }
}
var planet: Planet {
get { storage[.planet] ?? .earth }
set { storage[.planet] = newValue }
}
}Таким образом в необходимом месте достаточно будет создать экземпляр, и работать, непосредственно, с ним.
var settings = UserSettings()
settings.age = 20
settings.planet = .marsПричем не имеет значение, будет ли это singleton, environmentObject или объект, создаваемый на каждом экране. Выше рассмотрен последний вариант.
Заключение
В последнее время, с момента появления SwiftUI, все большей популярностью пользуются @propertyWrapper. Однако использование таких объектов накладывает дополнительные ограничения. К примеру, либо всегда нужно задавать значение по-умолчанию, либо все настройки будут опциональными. Более того, для того, чтобы работать с перечислениями и OptionSet, потребуется второй@propertyWrapper.
В самом разгаре WWDC 2022. К сожалению, Apple не предложили собственное решение для работы с устаревшим интерфейсом, несмотря на то, что сохранение настроек пользователя без сетевых вызовов - очень частая задача. Но это вовсе не означает, что нужно пользоваться тем, что есть, и пример тому был описан в этой статье.
Property wrappers in Swift
Create the Perfect UserDefaults Wrapper Using Property Wrapper
A better approach to writing a UserDefaults Property Wrapper
UserDefaults
WWDC 2022
