I'm trying to save my struct array. And I read all posts I could find for this topic. Finally I used one example and stripped it a little bit, but it doesn't work. It does not save, so it could not read back:
func path() -> String {
return URL(fileURLWithPath: "/Volumes/MacOS/fasttemp/Test.TXT").absoluteString
}
struct Movie {
let name: String
let releaseYear: Int
}
protocol Dictionariable {
func dictionaryRepresentation() -> NSDictionary
init?(dictionaryRepresentation: NSDictionary?)
}
extension Movie: Dictionariable {
func dictionaryRepresentation() -> NSDictionary {
let representation: [String: AnyObject] = [
"name": name as AnyObject,
"releaseYear": releaseYear as AnyObject
]
return representation as NSDictionary
}
init?(dictionaryRepresentation: NSDictionary?) {
guard let values = dictionaryRepresentation else {return nil}
if let name = values["name"] as? String,
let releaseYear = values["releaseYear"] as? Int {
self.name = name
self.releaseYear = releaseYear
} else {
return nil
}
}
}
func extractStructureFromArchive<T: Dictionariable>() -> T? {
guard let encodedDict = NSKeyedUnarchiver.unarchiveObject(withFile: path()) as? NSDictionary else {return nil}
return T(dictionaryRepresentation: encodedDict)
}
func archiveStructure<T: Dictionariable>(structure: T) {
let encodedValue = structure.dictionaryRepresentation()
NSKeyedArchiver.archiveRootObject(encodedValue, toFile: path())
}
func extractStructuresFromArchive<T: Dictionariable>() -> [T] {
guard let encodedArray = NSKeyedUnarchiver.unarchiveObject(withFile: path()) as? [AnyObject] else {return []}
return encodedArray.map{$0 as? NSDictionary}.flatMap{T(dictionaryRepresentation: $0)}
}
func archiveStructureInstances<T: Dictionariable>(structures: [T]) {
let encodedValues = structures.map{$0.dictionaryRepresentation()}
NSKeyedArchiver.archiveRootObject(encodedValues, toFile: path())
}
let movies = [
Movie(name: "Avatar", releaseYear: 2009),
Movie(name: "The Dark Knight", releaseYear: 2008)
]
// this fails!
archiveStructureInstances(structures: movies)
let someArray: [Movie] = extractStructuresFromArchive()
print("\(someArray[0].name)")
No file is created. The folder exist. What's wrong with it?
I'm using XCode 8.2.1 with Swift 3.
Added: I took this example from another question/answer. I also reduced it from 3 to 2 struct members. And I updated it for Swift 3. But it still doesn't work! That's the reason I'm asking.
Your path is wrong. You do not archive to a file. You have to use a directory.
Changing the path() methode to this (from this answer):
func path() -> String {
let documentsPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first
let path = documentsPath! + "/Movie"
return path
}
your code prints Avatar as expected.
Related
until version 10.7.6 of Realm I could convert to dictionary and then to json with this code below, but the ListBase class no longer exists.
extension Object {
func toDictionary() -> NSDictionary {
let properties = self.objectSchema.properties.map { $0.name }
let dictionary = self.dictionaryWithValues(forKeys: properties)
let mutabledic = NSMutableDictionary()
mutabledic.setValuesForKeys(dictionary)
for prop in self.objectSchema.properties as [Property] {
// find lists
if let nestedObject = self[prop.name] as? Object {
mutabledic.setValue(nestedObject.toDictionary(), forKey: prop.name)
} else if let nestedListObject = self[prop.name] as? ListBase { /*Cannot find type 'ListBase' in scope*/
var objects = [AnyObject]()
for index in 0..<nestedListObject._rlmArray.count {
let object = nestedListObject._rlmArray[index] as! Object
objects.append(object.toDictionary())
}
mutabledic.setObject(objects, forKey: prop.name as NSCopying)
}
}
return mutabledic
}
}
let parameterDictionary = myRealmData.toDictionary()
guard let postData = try? JSONSerialization.data(withJSONObject: parameterDictionary, options: []) else {
return
}
List now inherits from RLMSwiftCollectionBase apparently, so you can check for that instead. Also, this is Swift. Use [String: Any] instead of NSDictionary.
extension Object {
func toDictionary() -> [String: Any] {
let properties = self.objectSchema.properties.map { $0.name }
var mutabledic = self.dictionaryWithValues(forKeys: properties)
for prop in self.objectSchema.properties as [Property] {
// find lists
if let nestedObject = self[prop.name] as? Object {
mutabledic[prop.name] = nestedObject.toDictionary()
} else if let nestedListObject = self[prop.name] as? RLMSwiftCollectionBase {
var objects = [[String: Any]]()
for index in 0..<nestedListObject._rlmCollection.count {
let object = nestedListObject._rlmCollection[index] as! Object
objects.append(object.toDictionary())
}
mutabledic[prop.name] = objects
}
}
return mutabledic
}
}
Thanks to #Eduardo Dos Santos. Just do the following steps. You will be good to go.
Change ListBase to RLMSwiftCollectionBase
Change _rlmArray to _rlmCollection
Import Realm
struct ExpandableNames:Codable {
var isExpanded: Bool
var names: [String]
var icons: [String]
}
var names = ["Food/Drink", "Tours", "Transport", "Gifts","Flights", "Shopping", "Activities","Entertainment","Accomodation","Other"]
var icons = ["Food_Category", "Tours", "Transport_Category", "Gifts_Category","Flights_Category","Shopping_Category","Activities_category","Entertainment_category", "Accomodation_Category","Other_Category"]
var categoryWholeArray = [Int:ExpandableNames]()
How to store categoryWholeArray to user default?
I tried
Store userDefault:
UserDefaults.standard.set(try? PropertyListEncoder().encode(categoryWholeArray), forKey:"categoryWholeArray")
Retrieve data problem is here
if let data = UserDefaults.standard.value(forKey:"categoryWholeArray") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<categoryWholeArray.values>.self, from: data)
}
Anyone tried?
You need to encode data while storing in UserDefaults and when you try to get those data you must need to decode it.
Here is extension i have that might help you.
extension UserDefaults {
func setCustomObjToUserDefaults(CustomeObj: AnyObject, forKey:String) {
let defaults = UserDefaults.standard
let encodedData = NSKeyedArchiver.archivedData(withRootObject: CustomeObj)
defaults.set(encodedData, forKey: forKey)
defaults.synchronize()
}
func getCustomObjFromUserDefaults(forKey:String) -> AnyObject? {
let defaults = UserDefaults.standard
if defaults.object(forKey: forKey) != nil {
if let decoded = defaults.object(forKey: forKey) as? Data {
let decodedTeams = NSKeyedUnarchiver.unarchiveObject(with:decoded) as AnyObject
return decodedTeams
}
}
return nil
}
func setJsonObject<T: Encodable>(encodable: T, forKey key: String) {
if let data = try? JSONEncoder().encode(encodable) {
set(data, forKey: key)
}
}
func getJsonObject<T: Decodable>(_ type: T.Type, forKey key: String) -> T? {
if let data = object(forKey: key) as? Data,
let value = try? JSONDecoder().decode(type, from: data) {
return value
}
return nil
}
func removeCustomObject(forKey:String)
{
let defaults = UserDefaults.standard
defaults.removeObject(forKey: forKey)
defaults.synchronize()
}
}
All correct except
if let data = UserDefaults.standard.value(forKey:"categoryWholeArray") as? Data {
let songs2 = try? PropertyListDecoder().decode([Int:ExpandableNames].self, from: data)
print(songs2)
}
This fails (Non-nominal type 'Any' cannot be extended)
extension Any {
func literal() -> String {
if let booleanValue = (self as? Bool) {
return String(format: (booleanValue ? "true" : "false"))
}
else
if let intValue = (self as? Int) {
return String(format: "%d", intValue)
}
else
if let floatValue = (self as? Float) {
return String(format: "%f", floatValue)
}
else
if let doubleValue = (self as? Double) {
return String(format: "%f", doubleValue)
}
else
{
return String(format: "<%#>", self)
}
}
}
as I would like to use it in a dictionary (self) to xml string factory like
extension Dictionary {
// Return an XML string from the dictionary
func xmlString(withElement element: String, isFirstElement: Bool) -> String {
var xml = String.init()
if isFirstElement { xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") }
xml.append(String(format: "<%#>\n", element))
for node in self.keys {
let value = self[node]
if let array: Array<Any> = (value as? Array<Any>) {
xml.append(array.xmlString(withElement: node as! String, isFirstElemenet: false))
}
else
if let dict: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>) {
xml.append(dict.xmlString(withElement: node as! String, isFirstElement: false))
}
else
{
xml.append(String(format: "<%#>", node as! CVarArg))
xml.append((value as Any).literal
xml.append(String(format: "</%#>\n", node as! CVarArg))
}
}
xml.append(String(format: "</%#>\n", element))
return xml.replacingOccurrences(of: "&", with: "&", options: .literal, range: nil)
}
}
I was trying to reduce the code somehow, as the above snippet is repeated a few times in a prototype I'm building but this is not the way to do it (a working copy with the snippet replicated works but ugly?).
Basically I want to generate a literal for an Any value - previously fetched from a dictionary.
It seems like you can't add extensions to Any. You do have some other options though - either make it a function toLiteral(value: Any) -> String, or what is probably a neater solution; use the description: String attribute which is present on all types that conform to CustomStringConvertible, which includes String, Int, Bool, and Float - your code would be simplified down to just xml.append(value.description). You then just have make a simple implementation for any other types that you might get.
Ok, finally got this working. First the preliminaries: each of your objects needs to have a dictionary() method to marshal itself. Note: "k.###" are struct static constants - i.e., k.name is "name", etc. I have two objects, a PlayItem and a PlayList:
class PlayItem : NSObject {
var name : String = k.item
var link : URL = URL.init(string: "http://")!
var time : TimeInterval
var rank : Int
var rect : NSRect
var label: Bool
var hover: Bool
var alpha: Float
var trans: Int
var temp : String {
get {
return link.absoluteString
}
set (value) {
link = URL.init(string: value)!
}
}
func dictionary() -> Dictionary<String,Any> {
var dict = Dictionary<String,Any>()
dict[k.name] = name
dict[k.link] = link.absoluteString
dict[k.time] = time
dict[k.rank] = rank
dict[k.rect] = NSStringFromRect(rect)
dict[k.label] = label ? 1 : 0
dict[k.hover] = hover ? 1 : 0
dict[k.alpha] = alpha
dict[k.trans] = trans
return dict
}
}
class PlayList : NSObject {
var name : String = k.list
var list : Array <PlayItem> = Array()
func dictionary() -> Dictionary<String,Any> {
var dict = Dictionary<String,Any>()
var items: [Any] = Array()
for item in list {
items.append(item.dictionary())
}
dict[k.name] = name
dict[k.list] = items
return dict
}
}
Note any value so marshal has to be those legal types for a dictionary; it helps to have aliases so in the PlayItem a "temp" is the string version for the link url, and its getter/setter would translate.
When needed, like the writeRowsWith drag-n-drop tableview handler, I do this:
func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {
if tableView == playlistTableView {
let objects: [PlayList] = playlistArrayController.arrangedObjects as! [PlayList]
var items: [PlayList] = [PlayList]()
var promises = [String]()
for index in rowIndexes {
let item = objects[index]
let dict = item.dictionary()
let promise = dict.xmlString(withElement: item.className, isFirstElement: true)
promises.append(promise)
items.append(item)
}
let data = NSKeyedArchiver.archivedData(withRootObject: items)
pboard.setPropertyList(data, forType: PlayList.className())
pboard.setPropertyList(promises, forType:NSFilesPromisePboardType)
pboard.writeObjects(promises as [NSPasteboardWriting])
}
else
{
let objects: [PlayItem] = playitemArrayController.arrangedObjects as! [PlayItem]
var items: [PlayItem] = [PlayItem]()
var promises = [String]()
for index in rowIndexes {
let item = objects[index]
let dict = item.dictionary()
let promise = dict.xmlString(withElement: item.className, isFirstElement: true)
promises.append(promise)
items.append(item)
}
let data = NSKeyedArchiver.archivedData(withRootObject: items)
pboard.setPropertyList(data, forType: PlayList.className())
pboard.setPropertyList(promises, forType:NSFilesPromisePboardType)
pboard.writeObjects(promises as [NSPasteboardWriting])
}
return true
}
What makes this happen are these xmlString extensions and the toLiteral function - as you cannot extend "Any":
func toLiteral(_ value: Any) -> String {
if let booleanValue = (value as? Bool) {
return String(format: (booleanValue ? "1" : "0"))
}
else
if let intValue = (value as? Int) {
return String(format: "%d", intValue)
}
else
if let floatValue = (value as? Float) {
return String(format: "%f", floatValue)
}
else
if let doubleValue = (value as? Double) {
return String(format: "%f", doubleValue)
}
else
if let stringValue = (value as? String) {
return stringValue
}
else
if let dictValue: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>)
{
return dictValue.xmlString(withElement: "Dictionary", isFirstElement: false)
}
else
{
return ((value as AnyObject).description)
}
}
extension Array {
func xmlString(withElement element: String, isFirstElemenet: Bool) -> String {
var xml = String.init()
xml.append(String(format: "<%#>\n", element))
self.forEach { (value) in
if let array: Array<Any> = (value as? Array<Any>) {
xml.append(array.xmlString(withElement: "Array", isFirstElemenet: false))
}
else
if let dict: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>) {
xml.append(dict.xmlString(withElement: "Dictionary", isFirstElement: false))
}
else
{
xml.append(toLiteral(value))
}
}
xml.append(String(format: "<%#>\n", element))
return xml
}
}
extension Dictionary {
// Return an XML string from the dictionary
func xmlString(withElement element: String, isFirstElement: Bool) -> String {
var xml = String.init()
if isFirstElement { xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") }
xml.append(String(format: "<%#>\n", element))
for node in self.keys {
let value = self[node]
if let array: Array<Any> = (value as? Array<Any>) {
xml.append(array.xmlString(withElement: node as! String, isFirstElemenet: false))
}
else
if let dict: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>) {
xml.append(dict.xmlString(withElement: node as! String, isFirstElement: false))
}
else
{
xml.append(String(format: "<%#>", node as! CVarArg))
xml.append(toLiteral(value as Any))
xml.append(String(format: "</%#>\n", node as! CVarArg))
}
}
xml.append(String(format: "</%#>\n", element))
return xml
}
func xmlHTMLString(withElement element: String, isFirstElement: Bool) -> String {
let xml = self.xmlString(withElement: element, isFirstElement: isFirstElement)
return xml.replacingOccurrences(of: "&", with: "&", options: .literal, range: nil)
}
}
This continues another's solution, the toLiteral() suggestion above, in hopes it helps others.
Enjoy.
In the function below, everything works except when I try to get the variable partnerName from point A to point B, i.e. moving the variable in an out of a closure. I am a novice coder so I am hoping that someone can help me discover how to solve this particular issue and point me to a place where I might be able to learn the basics of how to share variables between different parts of a function.
func getAllConversations(handler: #escaping (_ conversationsArray: [Conversation]) -> ()) {
var partner = [""]
var title: String = ""
var partnerName = ""
var conversationsArray = [Conversation]()
REF_CONVERSATION.observeSingleEvent(of: .value) { (conversationSnapshot) in
guard let conversationSnapshot = conversationSnapshot.children.allObjects as? [DataSnapshot] else { return}
for conversation in conversationSnapshot {
let memberArray = conversation.childSnapshot(forPath: "members").value as! [String]
partner = memberArray.filter {$0 != (Auth.auth().currentUser?.uid)!}
if memberArray.contains((Auth.auth().currentUser?.uid)!) {
let newPartner = (String(describing: partner))
title = newPartner.replacingOccurrences(of: "[\\[\\]\\^+<>\"]", with: "", options: .regularExpression, range: nil)
databaseRef.child("bodhi").child(title).observeSingleEvent(of: .value, with: { (snapshot) in
if let bodhiDict = snapshot.value as? [String: AnyObject]
{
//I realize that the variable is being wrongly redeclared here to make this run.
let partnerName = (bodhiDict["Name"] as! String)
print ("partnerName returned from firebase: \(partnerName)")
// Point A: This prints "Sandy"
}
})
print ("partnerName: \(partnerName)")
// Point B: This prints nothing but if I add partnerName = "Sandy", then the function complete
title = partnerName
print ("new title: \(title)")
let conversation = Conversation(conversationTitle: title, key: conversation.key, conversationMembers: memberArray, conversationMemberCount: memberArray.count)
conversationsArray.append(conversation)
}
}
handler(conversationsArray)
}
}
func createGroup(withTitle title: String, andDescription description: String, forUserIds ids: [String], handler: #escaping (_ groupCreated: Bool) -> ()) {
REF_GROUPS.childByAutoId().updateChildValues(["title": title, "description": description, "members": ids])
// need to add code here for slow internet: if successful connection true else error
handler(true)
}
func getAllGroups(handler: #escaping (_ groupsArray: [Group]) -> ()) {
var groupsArray = [Group]()
REF_GROUPS.observeSingleEvent(of: .value) { (groupSnapshot) in
guard let groupSnapshot = groupSnapshot.children.allObjects as? [DataSnapshot] else { return}
for group in groupSnapshot {
let memberArray = group.childSnapshot(forPath: "members").value as! [String]
if memberArray.contains((Auth.auth().currentUser?.uid)!) {
let title = group.childSnapshot(forPath: "title").value as! String
let description = group.childSnapshot(forPath: "description").value as! String
let group = Group(title: title, description: description, key: group.key, members: memberArray, memberCount: memberArray.count)
groupsArray.append(group)
}
}
handler(groupsArray)
}
}
}
I recommend you read the documentation on closures:
Closures
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html
Closures: Capturing Values
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID103
How can I save my struct with NSCoding so that it doesn´t change even if the user
closes the app? I would appreciate it if you could also show me how to implement the missing code correctly.
UPDATE with two new functions below:
Here is my code:
struct RandomItems: Codable
{
var items : [String]
var seen = 0
init(items:[String], seen: Int)
{
self.items = items
self.seen = seen
}
init(_ items:[String])
{ self.init(items: items, seen: 0) }
mutating func next() -> String
{
let index = Int(arc4random_uniform(UInt32(items.count - seen)))
let item = items.remove(at:index)
items.append(item)
seen = (seen + 1) % items.count
return item
}
func toPropertyList() -> [String: Any] {
return [
"items": items,
"seen": seen
]
}
}
override func viewWillDisappear(_ animated: Bool) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(quotes), forKey:"quote2")
}
override func viewDidAppear(_ animated: Bool) {
if let data = UserDefaults.standard.value(forKey:"quote2") as? Data {
let quote3 = try? PropertyListDecoder().decode(Array<RandomItems>.self, from: data)
}
}
}
extension QuotesViewController.RandomItems {
init?(propertyList: [String: Any]) {
return nil
}
}
How can I make sure the whole Array is covered here?
For structs you should be using the new Codable protocol. It is available since swift 4 and is highly recommended.
struct RandomItems: Codable
{
var items: [String]
var seen = 0
}
extension RandomItems {
init?(propertyList: [String: Any]) {
...
}
}
// Example usage
let a = RandomItems(items: ["hello"], seen: 2)
let data: Data = try! JSONEncoder().encode(a)
UserDefaults.standard.set(data, forKey: "MyKey") // Save data to disk
// some time passes
let data2: Data = UserDefaults.standard.data(forKey: "MyKey")! // fetch data from disk
let b = try! JSONDecoder().decode(RandomItems.self, from: data2)
Update
It looks like the Original Poster is nesting the struct inside of another class. Here is another example where there struct is nested.
class QuotesViewController: UIViewController {
struct RandomItems: Codable
{
var items: [String]
var seen = 0
}
}
extension QuotesViewController.RandomItems {
init(_ items:[String])
{ self.items = items }
init?(propertyList: [String: Any]) {
guard let items = propertyList["items"] as? [String] else { return nil }
guard let seen = propertyList["seen"] as? Int else { return nil }
self.items = items
self.seen = seen
}
}
// example usage
let a = QuotesViewController.RandomItems(items: ["hello"], seen: 2)
let data: Data = try! JSONEncoder().encode(a)
UserDefaults.standard.set(data, forKey: "MyKey") // Save data to disk
// some time passes
let data2: Data = UserDefaults.standard.data(forKey: "MyKey")! // fetch data from disk
let b = try! JSONDecoder().decode(QuotesViewController.RandomItems.self, from: data2)