Как стать автором
Обновить

Swift. UserDefaults. Property wrapper

Время на прочтение4 мин
Количество просмотров6.3K
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

Теги:
Хабы:
Всего голосов 1: ↑1 и ↓0+1
Комментарии2

Публикации

Истории

Работа

iOS разработчик
24 вакансии
Swift разработчик
31 вакансия

Ближайшие события