Encode complex object swift 3.0 - ios

I already read several posts and tried every solution, nothing works in my case.
I am using this complex data structure and need to store array of DrawnObjects in file. But it crashing when it first came to encode a variable which itself is an array of Structure type. Any help ?
[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x1702668c0
enum ArchiverKeys : String
{
case imageView = "ImageView"
case stateArray = "StateArray"
case bezierPathArray = "BezierPathArray"
case reactangleArray = "ReactangleArray"
case deleted = "Deleted"
}
struct RectanglePath {
var point1: CGPoint
var point2: CGPoint
var point3: CGPoint
var point4: CGPoint
}
struct StateObject {
var isAdd = true
var path = String()
}
class DrawObject: NSObject , NSCoding {
var ImageView = UIImageView()
var arrStates = [StateObject]()
var arrBezierPaths = [UIBezierPath]()
var rects = [RectanglePath]()
var deleted = false
override init() {
ImageView = UIImageView()
arrStates = []
arrBezierPaths = []
rects = []
deleted = false
}
func encode(with archiver: NSCoder) {
archiver.encode(self.ImageView, forKey:ArchiverKeys.imageView.rawValue )
archiver.encode(self.arrStates, forKey: ArchiverKeys.stateArray.rawValue)
archiver.encode(self.arrBezierPaths, forKey:ArchiverKeys.bezierPathArray.rawValue )
archiver.encode(self.rects, forKey: ArchiverKeys.reactangleArray.rawValue)
archiver.encode(self.deleted, forKey: ArchiverKeys.deleted.rawValue)
}
required convenience init(coder unarchiver: NSCoder) {
self.init()
self.ImageView = unarchiver.decodeObject(forKey: ArchiverKeys.imageView.rawValue) as! UIImageView
self.arrStates = unarchiver.decodeObject(forKey: ArchiverKeys.stateArray.rawValue) as! [StateObject]
self.arrBezierPaths = unarchiver.decodeObject(forKey: ArchiverKeys.bezierPathArray.rawValue) as! [UIBezierPath]
self.rects = unarchiver.decodeObject(forKey: ArchiverKeys.reactangleArray.rawValue) as! [RectanglePath]
self.deleted = (unarchiver.decodeObject(forKey: ArchiverKeys.deleted.rawValue) != nil)
}
}
func saveArrayTo(_ directoryName: String , arrayToSave: NSArray) {
// let encodedData = NSKeyedArchiver.archivedData(withRootObject: arrayToSave)
NSKeyedArchiver.archiveRootObject(arrayToSave, toFile: directoryName)
}
func loadArrayFrom(_ directoryName: String ) -> NSArray? {
let result = NSKeyedUnarchiver.unarchiveObject(withFile: directoryName)
return result as? NSArray
}

You cannot encode a Swift struct out of the box, you have to add a computed property and an init method to make the struct property list compliant
struct StateObject {
var isAdd = true
var path = ""
init(propertyList : [String:Any]) {
self.isAdd = propertyList["isAdd"] as! Bool
self.path = propertyList["path"] as! String
}
var propertyListRepresentation : [String:Any] {
return ["isAdd" : isAdd, "path" : path]
}
}
Now you can archive the array
archiver.encode(self.arrStates.map{$0.propertyListRepresentation}, forKey: ArchiverKeys.stateArray.rawValue)
and unarchive it
let states = unarchiver.decodeObject(forKey: ArchiverKeys.stateArray.rawValue) as! [[String:Any]]
self.arrStates = states.map { StateObject(propertyList: $0) }
Alternatively leave StateObject unchanged and
in encode(with replace the line
archiver.encode(self.arrStates, forKey: ArchiverKeys.stateArray.rawValue)
with
let arrStatesAsPlist = arrStates.map { return ["isAdd" : $0.isAdd, "path" : $0.path] }
archiver.encode(arrStatesAsPlist, forKey:ArchiverKeys.stateArray.rawValue)
in init(coder replace the line
archiver.encode(self.arrStates, forKey: ArchiverKeys.stateArray.rawValue)
with
let arrStatesAsPlist = unarchiver.decodeObject(forKey: ArchiverKeys.stateArray.rawValue) as! [[String:Any]]
arrStates = arrStatesAsPlist.map { StateObject(isAdd: $0["isAdd"] as! Bool, path: $0["path"] as! String) }
Notes:
Since you are assigning default values to all properties you can delete the entire init() method and the init() call in init(coder.
Don't use NSArray in Swift. Use a native Array
It's not a good idea to archive an UI element like UIImageView. Archive the image data.

Related

Feature layer markers not getting displayed in Esri ios SDK

I'm using ESRI iOS SDK 10.2.5,my app was built using swift 2.3 and i have converted it to 3.2 now,everythng works fine.
but there's a little problem in displaying the markers in feature layer which used to work correctly in swift 2.3
the method signature have slightly changed after migration .
here's the method signature for 2.3 (works fine):
func featureLayer(featureLayer: AGSFeatureLayer!, operation op: NSOperation!, didQueryFeaturesWithFeatureSet featureSet: AGSFeatureSet!) {
if (featureLayer == self.featureLayer) {
// featureset has more than 1 element
for poi in featureSet.features {
var attr = [String: AnyObject]()
attr["NAME"] = poi.allAttributes()[LanguageController.localizedString("poi_detail_name")] as? String;
attr["DETAIL"] = poi.allAttributes()["PHONE_NUMBER"] as? String;
attr["CATEGORY_EN"] = poi.allAttributes()["FACILITY_CAT_EN"] as? String;
attr["IS_OFFICE"] = false;
attr["IS_HDKP"] = false
let graphic = AGSGraphic(
geometry: poi.geometry,
symbol: getSymbolForGraphic(poi as! AGSGraphic),
attributes: attr)
self.mapLayer.addGraphic(graphic);
}
SVProgressHUD.dismiss()
}
method signature for swift 3.2 :
func featureLayer(_ featureLayer: AGSFeatureLayer!, operation op: Operation!, didQueryFeaturesWith featureSet: AGSFeatureSet!) {
if (featureLayer == self.featureLayer) {
// problem here,featureset.feature has 0 elements
for poi in featureSet.features {
var attr = [String: Any]()
attr["NAME"] = (poi as AnyObject).allAttributes()[LanguageController.localizedString("poi_detail_name")] as? String;
attr["DETAIL"] = (poi as AnyObject).allAttributes()["PHONE_NUMBER"] as? String;
attr["CATEGORY_EN"] = (poi as AnyObject).allAttributes()["FACILITY_CAT_EN"] as? String;
attr["IS_OFFICE"] = false as Any
attr["IS_HDKP"] = false as Any
let graphic = AGSGraphic(
geometry: (poi as AnyObject).geometry,
symbol: getSymbolForGraphic(poi as! AGSGraphic),
attributes: attr)
self.mapLayer.addGraphic(graphic);
}
SVProgressHUD.dismiss()
}
Model class for manipulating features :
class POICategory: NSObject, NSCoding
{
var isActive : Bool;
var name : String;
var nameFilter : String;
var key : String;
init(isActive: Bool,name : String, nameFilter: String, key: String){
self.isActive = isActive;
self.name = name;
self.nameFilter = nameFilter;
self.key = key;
}
required init?(coder aDecoder: NSCoder) {
self.isActive = aDecoder.decodeObject(forKey: "isActive") as? Bool
self.name = aDecoder.decodeObject(forKey: "name") as! String
self.nameFilter = aDecoder.decodeObject(forKey: "nameFilter") as! String
self.key = aDecoder.decodeObject(forKey: "key") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.isActive, forKey: "isActive")
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.nameFilter, forKey: "nameFilter")
aCoder.encode(self.key, forKey: "key")
}
}
i have debugged the code and came to know that the featureSet.feature has no elements in swift 3.2 but has elements in 2.3,that is why it is not entering the loop and setting the graphics to the marker
why this happens,since this method is from esri's delegate..is this bug from their end ?
please help me in fixing this issue,if anyone has any idea on this
there's nothing wrong in migration from 2.3 to 3.2,only thing which might break according to me is the code in your model class
try changing your model class in the second init from
self.isActive = aDecoder.decodeObject(forKey: "isActive") as? Bool
to
self.isActive = aDecoder.decodeBool(forKey: "isActive")
you are trying to read a boolean value like object, which might be the issue

User Defaults not saving dictionary contents in swift 3

I am trying to add a key and a value to a dictionary then add this dictionary the user defaults and read back into a dictionary object. I have two questions that I would really appreciate any help in,
1) why is the dictionary being read from user defaults empty? Since I added a key and a value to the dictionary shouldn't those be saved to the dictionary I retrieve from user defaults?
let defaults = UserDefaults.standard;
var myDict = [String: String]()
myDict["key"] = "value"
defaults.setValue(myDict, forKey: "myDict")
let mydict2 = defaults.object(forKey: "myDict") as? [String: String] ?? [String:String]()
print(mydict2)
2) What can I do to this code if the dictionary stores a custom class that I created as a value or a key so if the dictionary was like this:
class Car {
var engineSize: Int
var color: String
init() {
engineSize = 2000
color = "blue"
}
}
class Boat {
var surfaceArea: Int
var weight: Int
init() {
surfaceArea = 3500
weight = 4000
}
}
var myDict = [Car: Boat]()
how can I save that second dict to user defaults and read it from there?
Thank you
EDIT:
This is the answer suggested by ebby94:
var myDict = [String:String]()
myDict["key"] = "value";
let data = NSKeyedArchiver.archivedData(withRootObject: myDict)
UserDefaults.standard.set(data, forKey: "myDict")
func foo()
{
guard let archivedData = UserDefaults.standard.value(forKey: "myDict") as? Data
else
{
print("failed1")
return
}
guard var unarchivedDictionary = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? [String:String]
else
{
print("failed2")
return
}
print(unarchivedDictionary["key"]!)
}
foo()
However this prints failed1, I'm assuming the data wasn't archived correctly. Can this be because I'm running it in playground?
If you want to save custom object to userDefault first you need to encode & decode variable then save using archive & get data using unarchive.
class Car {
var engineSize: Int
var color: String
init() {
engineSize = 2000
color = "blue"
}
// Decode
required convenience public init(coder decoder: NSCoder)
{
self.init()
if let engineSize = decoder.decodeObject(forKey: "engineSize") as? Int
{
self.engineSize = engineSize
}
if let color = decoder.decodeObject(forKey: "color") as? String
{
self.color = color
}
}
// Encode
func encodeWithCoder(coder : NSCoder)
{
if let engineSize = self.engineSize
{
coder.encode(engineSize, forKey: "engineSize")
}
if let color = self.color
{
coder.encode(color, forKey: "weight")
}
}
}
class Boat {
var surfaceArea: Int
var weight: Int
init() {
surfaceArea = 3500
weight = 4000
}
// Decode
required convenience public init(coder decoder: NSCoder)
{
self.init()
if let surfaceArea = decoder.decodeObject(forKey: "surfaceArea") as? Int
{
self.surfaceArea = surfaceArea
}
if let weight = decoder.decodeObject(forKey: "weight") as? Int
{
self.weight = weight
}
}
// Encode
func encodeWithCoder(coder : NSCoder)
{
if let surfaceArea = self.surfaceArea
{
coder.encode(surfaceArea, forKey: "surfaceArea")
}
if let weight = self.weight
{
coder.encode(weight, forKey: "weight")
}
}
You can't save a dictionary directly in UserDefaults. You'll have to convert the dictionary into data and save it and then retrieve the data and unarchive it into dictionary.
Archive and save to UserDefaults
let myDict = [String:String]()
let data = NSKeyedArchiver.archivedData(withRootObject: myDict)
UserDefaults.standard.set(data, forKey: "myDict")
Retrieve and unarchive the data to dictionary
guard let archivedData = UserDefaults.standard.value(forKey: "myDict") as? Data
else{return}
guard let unarchivedDictionary = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? [String:String]
else{return}
Userdefaults not work in Playground. you need to implement and start it in an App in Simulator

Persist data between app launches

I have a class to handle a simple note creator in my app. At the moment, notes are stored using an array of custom Note objects. How can I save the contents of this array when the app closes and load them again when the app is re-opened? I've tried NSUserDefaults, but I can't figure out how to save the array since it isn't just comprised of Strings.
Code:
Note.swift
class Note {
var contents: String
// an automatically generated note title, based on the first line of the note
var title: String {
// split into lines
let lines = contents.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
// return the first
return lines[0]
}
init(text: String) {
contents = text
}
}
var notes = [
Note(text: "Contents of note"),]
There are different approaches to this.
NSCoding
The easiest would be to adopt NSCoding, let Note inherit from NSObject and use NSKeyedArchiver and NSKeyedUnarchiver to write to/from files in the app's sandbox.
Here is a trivial example for this:
final class Feedback : NSObject, NSCoding {
private static let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let content : String
let entry : EntryId
let positive : Bool
let date : NSDate
init(content: String, entry: EntryId, positive : Bool, date :NSDate = NSDate()) {
self.content = content
self.entry = entry
self.positive = positive
self.date = date
super.init()
}
#objc init?(coder: NSCoder) {
if let c = coder.decodeObjectForKey("content") as? String,
let d = coder.decodeObjectForKey("date") as? NSDate {
let e = coder.decodeInt32ForKey("entry")
let p = coder.decodeBoolForKey("positive")
self.content = c
self.entry = e
self.positive = p
self.date = d
}
else {
content = ""
entry = -1
positive = false
date = NSDate()
}
super.init()
if self.entry == -1 {
return nil
}
}
#objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeBool(self.positive, forKey: "positive")
aCoder.encodeInt32(self.entry, forKey: "entry")
aCoder.encodeObject(content, forKey: "content")
aCoder.encodeObject(date, forKey: "date")
}
static func feedbackForEntry(entry: EntryId) -> Feedback? {
let path = Feedback.documentsPath.stringByAppendingString("/\(entry).feedbackData")
if let success = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? Feedback {
return success
}
else {
return nil
}
}
func save() {
let path = Feedback.documentsPath.stringByAppendingString("/\(entry).feedbackData")
let s = NSKeyedArchiver.archiveRootObject(self, toFile: path)
if !s {
debugPrint("Warning: did not save a Feedback for \(self.entry): \"\(self.content)\"")
}
}
}
Core Data
The more efficient but more complex solution is using Core Data, Apple's ORM-Framework - which's usage is way beyond the scope of a SO answer.
Further Reading
NSHipster article
Archiving programming guide
Core Data programming guide

How to encode and decode struct to NSData in swift?

I have the following struct definition:
struct ThreadManager: Equatable {
let fid: Int
let date: NSDate
let forumName: String
let typeid: Int
var page: Int
var threadList: [Thread]
var totalPageNumber: Int?
}
and the thread is :
struct Thread: Equatable {
let author: Author
let replyCount: Int
let readCount: Int
let title: String
let tid: Int
let isTopThread: Bool
var attributedStringDictionary: [String: NSAttributedString]
var postDescripiontTimeString: String
var hasRead: Bool
}
How can I encode a ThreadManager variable to NSData? I tried to used the following functions, but it does not worK.
func encode<T>(var value: T) -> NSData {
return withUnsafePointer(&value) { p in
NSData(bytes: p, length: sizeofValue(value))
}
}
func decode<T>(data: NSData) -> T {
let pointer = UnsafeMutablePointer<T>.alloc(sizeof(T))
data.getBytes(pointer, length: sizeof(T))
return pointer.move()
}
I have ThreadManager items, and I want to store them into sqlite. So I need to convert them to NSData. I have a variable called threadManager, the number of items in its threadList is about 70. I run the code and set a breakpoint, and input encode(threadManager) in xcode console, it is only 73bytes. It is wrong. How can I encode and decode those struct to NSData.
If your database is to be read on any other platform (Android, the web, wherever), you'd better choosing a cross-platform format such as JSON, or spread your struct members in their dedicated columns in a database table.
If you only target iOS/OSX/tvOS/etc, I recommend NSCoder. It is efficient, and most importantly:
NSCoder is platform-independant, which means that your NSData coding and decoding is not dependent on the particular memory layout currently used by the platform. For example, you don't have to fear 32 / 64 bits compatibility.
NSCoder lets you change your type over time, while keeping the ability to import old versions of your struct.
The code below adds a asData() function to your struct, and an init(data:) initializer. Those two let you go back and forth from your struct to NSData.
import Foundation
struct MyStruct {
let name: String
let date: NSDate
}
extension MyStruct {
init(data: NSData) {
let coding = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! Coding
name = coding.name as String
date = coding.date
}
func asData() -> NSData {
return NSKeyedArchiver.archivedDataWithRootObject(Coding(self))
}
class Coding: NSObject, NSCoding {
let name: NSString
let date: NSDate
init(_ myStruct: MyStruct) {
name = myStruct.name
date = myStruct.date
}
required init?(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObjectForKey("name") as! NSString
self.date = aDecoder.decodeObjectForKey("date") as! NSDate
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "name")
aCoder.encodeObject(date, forKey: "date")
}
}
}
let encodedS = MyStruct(name: "foo", date: NSDate())
let data = encodedS.asData()
let decodedS = MyStruct(data: data)
print(decodedS.name)
print(decodedS.date)
#Gwendal Roué : you are right, but I have to build another class according to each struct. I used the following method, it is ugly, but it works. Can you help me to improve it?
init(data: NSData) {
let dictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! NSDictionary
fid = (dictionary["fid"] as! NSNumber).integerValue
date = dictionary["date"] as! NSDate
forumName = dictionary["forumName"] as! String
typeid = (dictionary["typeid"] as! NSNumber).integerValue
page = (dictionary["page"] as! NSNumber).integerValue
totalPageNumber = (dictionary["totalPageNumber"] as? NSNumber)?.integerValue
let threadDataList = dictionary["threadDataList"] as! [NSData]
threadList = threadDataList.map { Thread(data: $0) }
}
extension ThreadManager {
func encode() -> NSData {
let dictionary = NSMutableDictionary()
dictionary.setObject(NSNumber(integer: fid), forKey: "fid")
dictionary.setObject(date, forKey: "date")
dictionary.setObject(forumName, forKey: "forumName")
dictionary.setObject(NSNumber(integer: typeid), forKey: "typeid")
dictionary.setObject(NSNumber(integer: page), forKey: "page")
if totalPageNumber != nil {
dictionary.setObject(NSNumber(integer: totalPageNumber!), forKey: "totalPageNumber")
}
let threadDataList: [NSData] = threadList.map { $0.encode() }
dictionary.setObject(threadDataList, forKey: "threadDataList")
return NSKeyedArchiver.archivedDataWithRootObject(dictionary)
}
}

Storing and reading array of own classes into a plist?

I try to store a list of own classes into a plist. For that I also found a similar question already answered, but I still do not succeed doing this.
My code:
My Class:
class Bookmark: NSObject {
var sName:String="";
var sTextPosition:String=""; // beim Text scrollpos
init (coder aDecoder: NSCoder!) {
self.sName = aDecoder.decodeObjectForKey("name") as! String;
self.sTextPosition = aDecoder.decodeObjectForKey("textposition")as! String;
}
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeObject(sName, forKey:"name");
aCoder.encodeObject(sTextPosition, forKey:"textposition");
}
}
My Code for storing an array of bookmarks:
var bookmark1:Bookmark=Bookmark(); // <--"Missing argument for parameter 'coder' in call".
bookmark1.sName="myname";
bookmark1.sTextPosition="100";
let listofbookmarks:[Bookmark];
listofbookmarks.append(bookmark1)
let fileManager = (NSFileManager.defaultManager())
let directorys : [String]? = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.AllDomainsMask, true) as? [String]
if (directorys != nil){
let directories:[String] = directorys!;
let pathToFile = directories[0]; //documents directory
let plistfile = "BookmarkArray.plist"
let plistpath = pathToFile.stringByAppendingPathComponent(plistfile);
if !fileManager.fileExistsAtPath(plistpath){
println("Declaring cocoaArray")
var cocoaArray : NSArray = listofbookmarks;
cocoaArray.writeToFile(plistpath, atomically: true)
}
}
I stuck in creating a Bookmark : var bookmark1:Bookmark=Bookmark();
"Missing argument for parameter 'coder' in call".
Can someone help ?
The only initializer that you have in your object is init (coder aDecoder: NSCoder!) so when the object is being created it's assuming that you are calling that method and are missing an NSCoder object.
You also need to use NSKeyedArchiver to perform the serialization and write the data to storage. NSKeyedUnArchiver performs the reverse. To use them you adhere to the NSCoding protocol. You also need to mark your class as #objc so that it can be deserialized properly.
Here's a version of your code with an initializer method for creation without a NSCoder, plus a few other fixes:
import Foundation
#objc(Bookmark) class Bookmark: NSObject, NSCoding { // <- added #objc and NSCoding
var sName = ""
var sTextPosition = "" // beim Text scrollpos
init(name:String, position:String) { // <- new initializer
self.sName = name
self.sTextPosition = position
}
required init (coder aDecoder: NSCoder) { // <- removed ! to adhere to NSCoding protocol
if let name = aDecoder.decodeObjectForKey("name") as? String {
sName = name
}
if let position = aDecoder.decodeObjectForKey("textposition") as? String {
sTextPosition = position
}
}
func encodeWithCoder(aCoder: NSCoder) { // <- removed ! to adhere to NSCoding protocol
aCoder.encodeObject(sName, forKey:"name")
aCoder.encodeObject(sTextPosition, forKey:"textposition")
}
}
var listofbookmarks = [Bookmark]() // <- needed to create a mutable array instance
listofbookmarks.append(Bookmark(name: "myname", position: "100")) // <- new initializer
let fileManager = NSFileManager.defaultManager()
if let directories = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.AllDomainsMask, true) as? [String] {
if !directories.isEmpty {
let plistpath = directories[0].stringByAppendingPathComponent("BookmarkArray.plist")
if !fileManager.fileExistsAtPath(plistpath) {
NSKeyedArchiver.archiveRootObject(listofbookmarks, toFile: plistpath) // <- added serializer
}
if fileManager.fileExistsAtPath(plistpath) {
if let decoded = NSKeyedUnarchiver.unarchiveObjectWithFile(plistpath) as? [Bookmark] {
println(decoded[0].sName) // => "myname"
println(decoded[0].sTextPosition) // => "100"
}
}
}
}

Resources