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

Swift. UserDefaults. Property wrapper

Время на прочтение4 мин
Количество просмотров6.8K
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 разработчик
25 вакансий
Swift разработчик
41 вакансия

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

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань