Issue with UserDefaults (converting data to array and back) - swift

What I want to do:
I want to get an array from UserDefaults that I saved beforehand and append a custom object to it. Afterwards I want to encode it as a Data-type again and set this as the UserDefaults Key again.
My problem:
The encoding part is what is not working as intended for me.
It says: -[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x60000011a540
But I do not know how to fix this.
Below is my code for more context:
do {
let decoded = defaults.object(forKey: "ExArray") as! Data
var exo = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! [Exerc]
exo.append(datas[indexPath.row])
let enco = try NSKeyedArchiver.archivedData(withRootObject: exo, requiringSecureCoding: false) <- Here is the error
defaults.set(enco, forKey: "ExArray")
} catch {
print("Error encoding custom object NOSEARCHO")
}
This is how Exerc looks:
struct Exerc: Codable {
var title: String
var exID: String
}

Seems like you are not using the archiver features, so why don't you just use the codable?
do {
let key = "ExArray"
let decoded = defaults.data(forKey: key)!
var exo = try JSONDecoder().decode([Exerc].self, from: decoded)
exo.append(datas[indexPath.row])
let enco = try JSONEncoder().encode(exo)
defaults.set(enco, forKey: key)
} catch {
print("Error encoding/decoding custom object NOSEARCHO", error)
}
It just a simple refactored MVP of the original code, but you can even work a bit on this and make it human readable right in the plist file!

Related

Conversion of JSON String to Object always returns nil

I'm fairly new to this. Anyway, here we go:
I have JSON data that comes from an API. For the sake of this question, I have simplified it greatly. You can run the following code in a Playground.
import UIKit
struct Book: Codable {
let image: String
}
// this comes from my API
let jsonString = "{ \"image\" = \"someURL\" }"
print(jsonString) // { "image" = "someURL" }
// convert String to Data
let jsonData = jsonString.data(using: .utf8)
// decode data (in my project, I catch the error, of course)
let decoder = JSONDecoder()
let decodingResult = try? decoder.decode(Book.self, from: jsonData!)
print(decodingResult) // nil
As you can see, I'm trying to decode my JSON-String into an Object (my Struct), but the Decoder always returns nil.
Can someone point me in the right direction?
Thank you.
Your current jsonString isn't a proper JSON. Change it to "{ \"image\": \"someURL\" }", and it should work. For more information on JSON syntax, check this manual.

Can't unarchive custom Object with NSKeyedUnarchiver in AR Multiuser

I'm using MultipeerConnectivity to share SCNNodes position in a multiuser AR session.
When I archive (with NSKeyedArchiver.archivedData(withRootObject: someARNode, requiringSecureCoding: true) )
And unarchive (with if let node = try NSKeyedUnarchiver.unarchivedObject(ofClass:SCNNode.self, from: data) {)
Everything works fine, but now I'm trying to send a custom Object like this:
struct ARUser: Codable {
var idUser : String
var position: [Double]
}
When I try to unarchive the object received with the NSKeyedUnarchiver.unarchivedObject it let me error.
if let node = try NSKeyedUnarchiver.unarchivedObject(ofClass:ARUser.self, from: data) {...}
I get the syntax error: Incorrect argument label in call (have 'ofClass:from:', expected 'ofClasses:from:')
But if I change the function as suggested by the compiler:
if let node = try NSKeyedUnarchiver.unarchivedObject(ofClasses:[ARUser.self], from: data) {..}
I get the next syntax error: Cannot convert value of type 'ARUser.Type' to expected element type 'AnyObject.Type'
So, the question here is, what's the correct way to unarchive custom Objects?
Since here you use Codable
struct ARUser: Codable {
Then
do {
let dataToObject = try JSONDecoder().decode(ARUser.self,from:data)
let objectToData = try JSONEncoder().encode(dataToObject)
}
catch {
print(error)
}
NSKeyedUnarchiver is an old Objective-C stuff

Casting from Any to anything else fails

API gives me back a variable that has type Any. It looks like this when I print it.
{
"sender" : "Kira",
"created" : "08.05.2018",
"text" : "Cncncm"
}
I tried to use SwiftyJSON to cast it like this let mydata = JSON(data) but it failes. I tried to use Swift 4 decoding technique but that failed as well. I tried to do this let myData = data as? Dictionary<String, String> but it fails again.
I am clueless what to do here. Any tips or solutions?
Finally a chance to demonstrate one of the Codable protocols hidden gems. Please run the following in a Playground:
import Cocoa
let jsonData = """
{
"sender" : "Kira",
"created" : "08.05.2018",
"text" : "Cncncm"
}
""".data(using: .utf8)!
struct SenderText: Codable {
let sender: String
let created: Date
let text: String
}
let dayFormatter = DateFormatter()
dayFormatter.dateFormat = "dd.MM.yyyy"
let date = dayFormatter.date(from:"08.05.2018")
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dayFormatter)
do {
let sendText = try decoder.decode(SenderText.self, from: jsonData)
print(sendText)
} catch {
print(error)
}
The sheer elegance of how easy it is to define such an intricate parser mapping a messy JSON-string to your favourite struct will hardly ever stop to amaze me. No matter how weird your date format looks, it is hardly more than 3 lines away from being parsed during the process.
There is something in regard to casting you should note though: In Swift, as in most object oriented languages, you can only cast something to something else if (and only if) it already is something else in the first place (but that knowledge has been lost somewhere). Since your String is "just" a String (in disguise of an Any maybe) you won't be able to cast it to anything else. However the Codable protocol provides you with a terrific means to decode from the Strings Data with astonishing ease. This process should not be mistaken as a cast, even if it looks largely the same. It is the creation and initialisation of another, more fittingly structured object from a simple piece of Data that you are likely to have gotten from your average web service of choice.
Great so far, at least in my book.
You can parse it like this as it's a json string
let trd = yourVar as? String
if let data = trd?.data(using: String.Encoding.utf8) {
do {
var content = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String:String]
print(content)
}
catch let error as NSError {
print(error)
}
}

Dictionary from Plist gives inconsistent results with same key

Inspired by this question:
Is there a way of getting a Mac's icon given its model number?
I was working around getting similar results but using the Mac's model identifier as a start, not the Mac's model number.
But I'm stuck with a weird problem when using my Mac's model identifier to find the related system icon.
On my office machine I get "iMac14,2" as a model identifier.
When I load this plist as a dictionary...
/System/Library/PrivateFrameworks/ServerInformation.framework/Versions/A/Resources/English.lproj/SIMachineAttributes.plist
...I see that it has keys for all Mac models, including "iMac14,2", and the values contain, among other things, the URL for the icon.
However, when I try to grab this dictionary's value for the identifier key ("iMac14,2") I got nil, although I get the actual value if I grab it with a literal key.
But the literal key and the key I get from my modelIdentifier function are the same. It looks like it anyway...
To grab the model identifier:
func modelIdentifier() -> String? {
let service: io_service_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice").takeUnretainedValue())
let cfstr = "model" as CFString
if let model = IORegistryEntryCreateCFProperty(service, cfstr, kCFAllocatorDefault, 0).takeUnretainedValue() as? NSData {
if let nsstr = NSString(data: model, encoding: NSUTF8StringEncoding) {
return String(nsstr)
}
}
return nil
}
if let id = modelIdentifier() {
println(id) // prints "iMac14,2"
}
Finding the value with this result fails:
if let dict = NSDictionary(contentsOfFile: "/System/Library/PrivateFrameworks/ServerInformation.framework/Versions/A/Resources/English.lproj/SIMachineAttributes.plist") as? [String:AnyObject] {
if let id = modelIdentifier() {
if let result = dict[id] as? [String:AnyObject] {
print(result) // nil
}
}
}
But if I do the same with a literal string it works:
if let result = dict["iMac14,2"] as? [String:AnyObject] {
print(result)
}
result:
[architecture: x86_64, LOCALIZABLE: {
description = "iMac with 27\" widescreen LED-backlit display, introduced late 2013.";
marketingModel = "27\" iMac (Late 2013)";
model = iMac;
processor = "Quad-core Intel Core i5, Quad-core Intel Core i7";
}, hardwareImageName: /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/com.apple.imac-unibody-27-no-optical.icns]
What is wrong here?
The strings look the same but aren't the same?
Or am I missing something else?
The NSData created by IORegistryEntryCreateCFProperty for the model key contains a NUL-terminated string. You're including that NUL in your NSString.
Create your string this way instead:
if let nsstr = NSString(CString: UnsafePointer<Int8>(model.bytes), encoding: NSUTF8StringEncoding) {
return String(nsstr)
}

Content Blocker extension with a String instead of a file

I'm using the function NSItemProvider(contentsOfURL: NSBundle.mainBundle().URLForResource("blockerList", withExtension: "json") in a content blocker extension.
The thing is that all my rules are stored in a few dictionaries and, when I'm using this function, it's always because the rules have changed. I'm currently creating a String from these dictionaries that looks like "[{\"trigger\": {\"url-filter\": \"webiste.com\"},\"action\": {"\type\": \"css-display-none\",\"selector\":\".testContentBlocker\"}}]"and I have to transform it in a JSON file to finally be able to use it in the function written previously described.
Instead of having to put the String in a JSON file to be able to use it, could I do something simpler to use NSItemProvider()?
By loading the extension up in the debugger (and by using Hopper), you can see that NSItemProvider(contentsOfURL:) is simply registering to provide data from the file's contents with type public.json.
(lldb) po attachment
<NSItemProvider: 0x7fd4c250f2a0> {types = (
"public.file-url",
"public.json"
)}
It's roughly equivalent to this:
// possible implementation of NSItemProvider.init(contentsOfURL:)
convenience init?(contentsOfURL fileURL: NSURL!)
{
self.init(item: fileURL, typeIdentifier: (fileURL.fileURL ? kUTTypeFileURL : kUTTypeURL) as String)
let type = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension, fileURL.pathExtension!, nil)?.takeRetainedValue() as! String
registerItemForTypeIdentifier(type) { completionHandler, expectedValueClass, options in
let data = try! NSData(contentsOfURL: fileURL, options: .DataReadingMappedAlways)
completionHandler(data, nil)
}
}
So you can do this yourself, in memory:
// get the data
let data = NSData(contentsOfURL: NSBundle.mainBundle().URLForResource("blockerList", withExtension: "json")!)
// put the data in an item provider
let attachment = NSItemProvider(item: data, typeIdentifier: kUTTypeJSON as String)
// send the item to Safari
let item = NSExtensionItem()
item.attachments = [attachment]
context.completeRequestReturningItems([item], completionHandler: nil);
If you want to provide content dynamically, you can use NSJSONSerialization to transform a dictionary into NSData at runtime.