2015-01-25 3 views
11

Одна из вещей, которая меня беспокоит о Swift и Cocoa вместе, работает с NSUserDefaults, потому что нет информации о типе, и всегда необходимо отображать результат objectForKey на то, что вы ожидаете получить. Это небезопасно и непрактично. Я решил решить эту проблему, сделав NSUserDefaults более практичным в Swift-land и, надеюсь, чему-то научиться на этом пути. Вот мои цели вначале:NSUserDefaults in Swift - реализация типа безопасности

  1. Безопасность полного типа: каждая клавиша имеет один тип, связанный с ним. При настройке значения должно приниматься только значение этого типа, и при получении значения результат должен выдаваться с правильным типом
  2. Глобальный список ключей, которые понятны по смыслу и содержанию. Список должен быть простым в создании, изменении и расширении.
  3. Чистый синтаксис, используя, по возможности, нижние индексы. Например, это будет будет идеальным:

    3.1. set: UserDefaults [.MyKey] = значение

    3.2. получим: пусть значение = UserDefaults [.MyKey]

  4. Поддержка классов, которые соответствуют протоколу NSCoding по автоматически [не] архивирования их

  5. Поддержка всех типов списков недвижимости, принятых 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 явно интерпретирует массивы и Интс, как структуры, а не как их аналогов класса 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 будет принимать какие-либо быстрые классы и быстрые массивы, словари, цифры (Интс, поплавки, Bools, и т.д., которые преобразовывают в NSNumber), струнные ...

к сожалению, AFAIK это не существует.

Вопрос:

Как я могу написать общую функцию (или коллекцию перегруженных родовых функций), общий тип T может быть любой ссылочный тип или любое значение тип, который Свифт может преобразовать ссылочный тип в Objective-C?


Решено: С помощью ответов я получил, я приехал на то, что я хотел. В случае, если кто-то хочет взглянуть на мое решение, это here.

+0

Я не думаю, что это имеет смысл. Пользовательские настройки по умолчанию предназначены для (несколько более удобного) способа хранения параметров вызова команд. Речь идет не о сохранении полных объектов. –

+0

@ThomasKilian В большинстве моих проектов я в конечном итоге сохраняю структуры данных, такие как '[[String: [Int]]]' и пользовательские объекты, такие как 'TimerInfo',' Person', 'Event', подобные вещи. Объекты не очень сложны и нет оснований для использования более сложных библиотек, таких как CoreData. Я не понимаю, почему это не имеет смысла. Кроме того, это отличная возможность для изучения. – Alex

+1

Я обычно заканчиваю сериализацию для тех объектов, которые сводят его к минимальному количеству строк/ints/floats. Конечно, YMMV. Но, поскольку вы не разделяете пользовательские значения по умолчанию, я думаю, что безопасность типа имеет меньшее значение.Я бы просто создал класс оболочки вокруг него, чтобы убедиться, что вы получаете то, что хотите. –

ответ

4

Я не хочу хвастаться, но ... о, кто я шучу, я totally do!

Preferences.set([NSData()], forKey: "MyKey1") 
Preferences.get("MyKey1", type: type([NSData])) 
Preferences.get("MyKey1") as [NSData]? 

func crunch1(value: [NSData]) 
{ 
    println("Om nom 1!") 
} 

crunch1(Preferences.get("MyKey1")!) 

Preferences.set(NSArray(object: NSData()), forKey: "MyKey2") 
Preferences.get("MyKey2", type: type(NSArray)) 
Preferences.get("MyKey2") as NSArray? 

func crunch2(value: NSArray) 
{ 
    println("Om nom 2!") 
} 

crunch2(Preferences.get("MyKey2")!) 

Preferences.set([[String:[Int]]](), forKey: "MyKey3") 
Preferences.get("MyKey3", type: type([[String:[Int]]])) 
Preferences.get("MyKey3") as [[String:[Int]]]? 

func crunch3(value: [[String:[Int]]]) 
{ 
    println("Om nom 3!") 
} 

crunch3(Preferences.get("MyKey3")!) 
+0

Да! Спасибо, протокол '_ObjectiveCBridgeable' именно то, что я хотел! – Alex

+0

Ups, yep ... но считайте, что префикс '_' предлагает некоторую осторожность при использовании его в производственном коде. –

+0

Обычно '_' используется для недокументированных протоколов внутренних типов и т. Д. Это означает, что они могут измениться в будущем без какой-либо связи/документации. Swift все еще находится в огромной эволюции, поэтому это решение может быть нестабильным в _time_. Если это не часть частного api, вы можете использовать его ... просто знайте. В любом случае это может быть смягчено тем фактом, что гарантировано, что ваше приложение будет иметь текущую быструю среду выполнения в комплекте. Таким образом, как только он будет распространен, он будет работать даже после обновлений Apple. –

-1

Я хотел бы представить свою идею. (Извините за мой плохой английский заранее.)

let plainKey = UDKey("Message", string) 
let mixedKey 
    = UDKey("Mixed" 
     , array(dictionary(
      string, tuple(
       array(integer), 
       optional(date))))) 

let ud = UserDefaults(NSUserDefaults.standardUserDefaults()) 
ud.set(plainKey, "Hello") 
ud.set(plainKey, 2525) // <-- compile error 
ud.set(mixedKey, [ [ "(^_^;)": ([1, 2, 3], .Some(NSDate()))] ]) 
ud.set(mixedKey, [ [ "(^_^;)": ([1, 2, 3], .Some(NSData()))] ]) // <-- compile error 

Единственное отличие состоит в том, что UDKey() теперь требует # 2 аргумента, значение BiMap класса. Я разобрал работу из UDKey в BiMap, которая преобразует значение типа в/из значения другого типа.

public class BiMap<A, B> { 
    public func AtoB(a: A) -> B? 
    public func BtoA(b: B) -> A? 
} 

Следовательно, типы, которые принимает set/get может, не проводят BiMap, и больше не ограничивается типами as может автоматически поданных из/в AnyObject (более конкретно, типы NSUserDefaults может, принимает.).

Потому что BiMap - это общий класс, вы можете легко создавать подтипы этого, заменяя произвольные два типа, которые вы хотите.

Полный исходный код. (Но есть ошибки еще фиксироваться ..) https://gist.github.com/hisui/47f170a9e193168dc946

Смежные вопросы