Одна из вещей, которые меня беспокоят о Swift и Cocoa вместе, работает с NSUserDefaults, потому что нет информации о типе, и всегда необходимо передать результат objectForKey
тому, что вы ожидаете получить. Это небезопасно и непрактично. Я решил решить эту проблему, сделав NSUserDefaults более практичным в Swift-land и, надеюсь, чему-то научиться на этом пути. Здесь были мои цели в начале:
- Полная безопасность типов: каждый ключ имеет один тип, связанный с ним. При настройке значения должно приниматься только значение этого типа, и при получении значения результат должен выдаваться с правильным типом
- Глобальный список ключей, которые понятны по смыслу и содержанию. Список должен быть простым в создании, изменении и расширении.
-
Очистить синтаксис, используя по возможности индексы. Например, это быть совершенным:
3,1. set: UserDefaults [.MyKey] = значение
3,2. get: let value = UserDefaults [.MyKey]
-
Поддержка классов, соответствующих протоколу NSCoding, автоматически [un] архивирование их
- Поддержка всех типов списков свойств, принятых NSUserDefaults
Я начал с создания этой общей структуры:
struct UDKey <T> {
init(_ n: String) { name = n }
let name: String
}
Затем я создал эту другую структуру, которая служит контейнером для всех ключей в приложении:
struct UDKeys {}
Затем это может быть расширено, чтобы добавлять ключи там, где это необходимо:
extension UDKeys {
static let MyKey1 = UDKey<Int>("MyKey1")
static let MyKey2 = UDKey<[String]>("MyKey2")
}
Обратите внимание, что каждый ключ имеет тип, связанный с ним. Он представляет тип информации для сохранения. Кроме того, свойство name является строкой, которая должна использоваться как ключ для NSUserDefaults.
Ключи могут быть перечислены все в одном файле констант или добавлены с использованием расширений для каждого файла в непосредственной близости от того места, где они используются для хранения данных.
Затем я создал класс UserDefaults, ответственный за обработку/настройку информации:
class UserDefaultsClass {
let storage = NSUserDefaults.standardUserDefaults()
init(storage: NSUserDefaults) { self.storage = storage }
init() {}
// ...
}
let UserDefaults = UserDefaultsClass() // or UserDefaultsClass(storage: ...) for further customisation
Идея заключается в том, что создается один экземпляр для определенного домена, и затем каждый метод получает доступ таким образом:
let value = UserDefaults.myMethod(...)
Я предпочитаю такой подход к таким вещам, как UserDefaults.sharedInstance.myMethod(...) (слишком долго!) или используя методы класса для всего. Кроме того, это позволяет одновременно взаимодействовать с различными доменами, используя более одного UserDefaultsClass с различными значениями хранения.
До сих пор были рассмотрены пункты 1 и 2, но теперь начинается сложная часть: как на самом деле разрабатывать методы на UserDefaultsClass, чтобы соответствовать остальным.
Например, начнем с пункта 4. Сначала я попробовал это (этот код находится внутри UserDefaultsClass):
subscript<T: NSCoding>(key: UDKey<T>) -> T? {
set { storage.setObject(NSKeyedArchiver.archivedDataWithRootObject(newValue), forKey: key.name) }
get {
if let data = storage.objectForKey(key.name) as? NSData {
return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? T
} else { return nil }
}
}
Но потом я узнаю, что Swift не разрешает общие индексы!! Хорошо, тогда я предполагаю, что мне придется использовать функции. Там идет половина пункта 3...
func set <T: NSCoding>(key: UDKey<T>, _ value: T) {
storage.setObject(NSKeyedArchiver.archivedDataWithRootObject(value), forKey: key.name)
}
func get <T: NSCoding>(key: UDKey<T>) -> T? {
if let data = storage.objectForKey(key.name) as? NSData {
return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? T
} else { return nil }
}
И это прекрасно работает:
extension UDKeys { static let MyKey = UDKey<NSNotification>("MyKey") }
UserDefaults.set(UDKeys.MyKey, NSNotification(name: "Hello!", object: nil))
let n = UserDefaults.get(UDKeys.MyKey)
Обратите внимание, как я не могу позвонить UserDefaults.get(.MyKey)
. Я должен использовать UDKeys.MyKey
. И я не могу этого сделать, потому что пока не существует статических переменных в общей структуре!
Затем попробуйте номер 5. Теперь это была головная боль и что там, где мне нужна большая помощь.
Типы списков свойств относятся к документам:
Объектом по умолчанию должен быть список свойств, то есть экземпляр (или для коллекций - комбинация экземпляров): NSData, NSString, NSNumber, NSDate, NSArray или NSDictionary.
Что в Swift означает Int
, [Int]
, [[String:Bool]]
, [[String:[Double]]]
и т.д. все типы списка свойств. Сначала я подумал, что могу просто написать это и доверять тому, кто использует этот код, чтобы помнить, что разрешены только типы plist:
func set <T: AnyObject>(key: UDKey<T>, _ value: T) {
storage.setObject(value, forKey: key.name)
}
func get <T: AnyObject>(key: UDKey<T>) -> T? {
return storage.objectForKey(key.name) as? T
}
Но как вы заметите, пока это прекрасно работает:
extension UDKeys { static let MyKey = UDKey<NSData>("MyKey") }
UserDefaults.set(UDKeys.MyKey, NSData())
let d = UserDefaults.get(UDKeys.MyKey)
Это не означает:
extension UDKeys { static let MyKey = UDKey<[NSData]>("MyKey") }
UserDefaults.set(UDKeys.MyKey, [NSData()])
И это не так:
extension UDKeys { static let MyKey = UDKey<[Int]>("MyKey") }
UserDefaults.set(UDKeys.MyKey, [0])
Даже это:
extension UDKeys { static let MyKey = UDKey<Int>("MyKey") }
UserDefaults.set(UDKeys.MyKey, 1)
Проблема в том, что все они являются допустимыми типами списков свойств, но Swift явно интерпретирует массивы и ints как структуры, а не как их сопоставления классов Objective-C. Однако:
func set <T: Any>(key: UDKey<T>, _ value: T)
тоже не будет работать, потому что тогда принимается любой тип значения, а не только те, у кого есть кузена класса Obj-C, а storage.setObject(value, forKey: key.name)
уже недействителен, поскольку значение должно быть ссылочным типом.
Если в Swift существовал протокол, который принимал любой ссылочный тип и любой тип значения, который может быть преобразован в ссылочный тип в Objective-C (например, [Int]
и другие примеры, которые я упоминаю), эта проблема будет решена:
func set <T: AnyObjectiveCObject>(key: UDKey<T>, _ value: T) {
storage.setObject(value, forKey: key.name)
}
func get <T: AnyObjectiveCObject>(key: UDKey<T>) -> T? {
return storage.objectForKey(key.name) as? T
}
AnyObjectiveCObject
будет принимать любые быстрые классы и быстрые массивы, словари, числа (ints, float, bools и т.д., которые преобразуются в NSNumber), строки...
К сожалению, AFAIK этого не существует.
Вопрос:
Как я могу написать общую функцию (или набор перегруженных общих функций), общий тип T которой может быть любым ссылочным типом или любым типом значения, который Swift может преобразовать в ссылочный тип в Objective-C?
Решено:. С помощью полученных ответов я пришел к тому, что хотел. Если кто-то хочет взглянуть на мое решение, здесь, это так.