Xcode
Xcode

Вряд ли можно представить хоть одно приложение, которое не требует сохранения настроек пользователя. Очевидно, что для этих целей существует масса решений, и каждое из них решает одну, конкретную задачу. Сегодня речь пойдет о постоянном хранилище данных UserDefaults и его использовании для хранения данных.

Реализация

Выполнив поиск существующих решений на просторах сети интернет были найдены следующие материалы:

  1. Property wrappers in Swift

  2. Create the Perfect UserDefaults Wrapper Using Property Wrapper

  3. A better approach to writing a UserDefaults Property Wrapper

Однако все они имеют несколько недостатков:

  1. Значение не может быть опциональным, вследствие чего требуется указывать значение по-умолчанию

  2. Значение не может быть перечислением или OptionSet

  3. Все они позволяют сохранить в хранилище неподдерживаемые типы данных, вследствие чего приложение аварийно завершает свою работу

Но если обратиться к первоисточнику и изучить 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. Но это вовсе не означает, что нужно пользоваться тем, что есть, и пример тому был описан в этой статье.

  1. Оригинал статьи

  2. UserDefaults

  3. AppStorage