Why `NSKeyedUnarchiver` is unable to decode for `Date` - ios

Recently, I was dealing with persistence on iOS with NSKeyedArchiver and NSKeyedUnarchiver. But I got some problem.
Here is the code:
import Foundation
class Model {
static let instance = Model()
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let TestArchiveURL = DocumentsDirectory.appendingPathComponent("t")
static func getInstance() -> Model {
return instance
}
var str = "String"
var date = Date()
func save() {
let coder = NSKeyedArchiver(requiringSecureCoding: false)
coder.encode(str, forKey: ModelPropertyKey.str)
coder.encode(date, forKey: ModelPropertyKey.date)
do {
try coder.encodedData.write(to: Model.TestArchiveURL)
} catch {
print("error write to file: " + error.localizedDescription)
}
do {
let data = try Data(contentsOf: Model.TestArchiveURL)
let decoder = try NSKeyedUnarchiver(forReadingFrom: data)
let strGot = decoder.decodeObject(forKey: ModelPropertyKey.str) as? String
let dateGot = decoder.decodeObject(forKey: ModelPropertyKey.date) as? Date
print("\(String(describing: strGot)) \(String(describing: dateGot))")
} catch {
print("Error read from file")
}
}
}
I encode data with encode(:forKey:) and gain persistence with encodedData.write(to:). Then I try to read the data from disk with NSKeyedUnarchiver(forReadingFrom:) and decodeObject(forKey:). But this strategy works for String and fails for Date. The print output:
Optional("String") nil
I wonder why this happened.

You need to specify the object type you are trying to decode and since you are decoding Date object you need to understand that NSKeyedArchiver and NSKeyedUnarchiver will bridge your Date Swift object to NSDate Objective-C object for compatibility between swift and obj-c because NSKeyedArchiver uses NSCoding and Date does not conform to NSCoding
To do that change your decoding to be like this
let dateGot = decoder.decodeObject(of: NSDate.self, forKey: ModelPropertyKey.date)! as Date
However I would suggest that you use the swift Codable protocol with JSONEncoder and JSONDecoder they have a really good dateDecodingStrategy which allows for specifying your custom date formatter when encoding and decoding and they also support decoding Date objects directly without bridging to obj-c NSDate that's good when your trying to send your object as a json payload or decoding it from a network call
here is an example
let encoder = JSONEncoder()
let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .short
encoder.dateEncodingStrategy = .formatted(formatter)
let data = try? encoder.encode(Date())
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let newDate = try? decoder.decode(Date.self, from: data!)

Related

JSON Parsing using multiple cores

iOS devices are getting better, have more cores but how can we get benefit out of it while we are parsing JSON?
Currently,
I am using JSONDecoder() for JSON Parsing. Is there way we can do it faster? Maybe using multiple threads parsing in parts etc.
Any hints/pointers will be appreciated.
import Foundation
let filePath = Bundle.main.path(forResource: "json", ofType: "json")
struct Vehicle: Codable {
private let color: String
private let tires: [Tire]
private let number: String
}
struct Tire: Codable {
private let company: String
private let isNew: Bool
}
func parseData(_ data: Data) {
let decoder = JSONDecoder()
try? decoder.decode([Vehicle].self, from: data)
}
func modifiedParsing(_ data: Data) {
}
let data = try String(contentsOfFile: filePath!).data(using: .utf8)
let date = Date()
let start = date.timeIntervalSince1970
//parseData(data!)
let end = Date().timeIntervalSince1970
print("Time Taken \(end-start)")
/*
Initial Times: [3.728722095489502, 3.5913820266723633, 3.5568389892578125, 3.534559965133667, 3.506725311279297]
After Changes
*/
I wanted to make JSON Parsing faster.
For anyone who is looking for a good solution please refer: https://github.com/bwhiteley/JSONShootout
Marshal is faster than codable.

How do I decode an object array and retrieve it from userdefaults?

I have an array with type [NotificationTriggers] that I would like to store in userdefaults. To do that, the data needs to be encoded and decoded. I have followed tutorials here:
https://cocoacasts.com/ud-5-how-to-store-a-custom-object-in-user-defaults-in-swift
and here:
https://www.hackingwithswift.com/example-code/system/how-to-load-and-save-a-struct-in-userdefaults-using-codable
But I still get an error that I can't seem to solve.
I have an extension of userDefaults where I do the magic in the get and set of the variable. NotificationTriggers Struct looks like this:
struct NotificationTriggers: Equatable, Codable {
var doorName: String
var notificationTrigger: String
}
Encoding seems to work, but in decoding I get an error saying
Cannot convert value of type '[Any]' to expected argument type 'Data'
This is the code:
extension UserDefaults {
var notificationTrigger: [NotificationTriggers] {
get {
if let data = self.array(forKey: UserDefaultsKey.notificationTrigger.rawValue) {
do {
let decoder = JSONDecoder()
//CODE BELOW PRODUCE ERROR
if let decodedData = try decoder.decode([NotificationTriggers]?.self, from: data) {
return decodedData
}
} catch { }
}
return []
}
set {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(newValue)
self.setValue(data, forKey: UserDefaultsKey.notificationTrigger.rawValue)
} catch { }
}
}
}
I have tried casting the data:
UserDefaultsKey.notificationTrigger.rawValue) as? Data // get warning "Cast from '[Any]?' to unrelated type 'Data' always fails"
UserDefaultsKey.notificationTrigger.rawValue) as? [NotificationTriggers] // get error "Cannot convert value of type '[NotificationTriggers]' to expected argument type 'Data'"
Not sure what's missing here. Any ideas?
You save Data for the key UserDefaultsKey.notificationTrigger.rawValue with:
let encoder = JSONEncoder()
let data = try encoder.encode(newValue)
self.setValue(data, forKey: UserDefaultsKey.notificationTrigger.rawValue)
So the first mistake I see:
if let data = self.array(forKey: UserDefaultsKey.notificationTrigger.rawValue) {
array(forKey:)? No, data(forKey:), you didn't save an Array, you saved a Data, a Data that might after some decoding "hides" an Array, but the system doesn't know it.
So, it should be:
if let data = self.data(forKey: UserDefaultsKey.notificationTrigger.rawValue) {
Then:
let decodedData = try decoder.decode([NotificationTriggers]?.self, from: data)
=>
let decodedData = try decoder.decode([NotificationTriggers].self, from: data)
Also, it's bad habit to have catch { }, if there is an error, you might want to know it:
catch {
print("Error while doingSomethingToCustomizeHere: \(error)")
}

Issue assigning variable value to open Apple Maps when pressing a UILabel

Essentially I am parsing JSON data and assigning it to a variable called addressPressNow I then have the following function that executes when a user taps on a UILabel:
The goal is to have Apple Maps open provided the variable value it contains.
Because I am assigning an address to a variable it will contain spaces
ex: 3981 Test Drive Cupertino CA 95014
NOTE: The value of the variable is being passed correctly because when I do print(addressPressNow) in func tapFunction it prints correctly.
#objc
func tapFunction(sender:UITapGestureRecognizer) {
let targetURL = NSURL(string: "http://maps.apple.com/?q=" + addressPressNow)!
UIApplication.shared.openURL(targetURL as URL)
}
The issue is I am having trouble applying the variable to the string URL with the following error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an
Optional value
The following is how I am assigning the value to the variable:
struct FacilityInfo: Decodable {
let address: String
class infoViewController: UIViewController {
var addressPressNow : String = ""
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(infoViewController.tapFunction))
addressInfo.isUserInteractionEnabled = true
addressInfo.addGestureRecognizer(tap)
let url = URL(string: "https://test/test/exampleā€¯)!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// ensure there is no error for this HTTP response
guard error == nil else {
print ("error: \(error!)")
return
}
// ensure there is data returned from this HTTP response
guard let data = data else {
print("No data")
return
}
// Parse JSON into array of Car struct using JSONDecoder
guard let cars = try? JSONDecoder().decode([FacilityInfo].self, from: data), let secondCar = cars.first
else {
print("Error: Couldn't decode data into cars array")
return
}
DispatchQueue.main.async {
self.addressPressNow = secondCar.facility_address
}
}
"I am assigning an address to a variable it will contain spaces"
If the address contains spaces then creating NSURL with the string will crash. You can use addingPercentEncoding to solve the problem
if let encodedAddress = addressPressNow.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
let targetURL = NSURL(string: "http://maps.apple.com/?q=" + encodedAddress)!
UIApplication.shared.openURL(targetURL as URL)
}
And don't use NSURL and force unwrapping. Update it like this
if let encodedAddress = addressPressNow.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let targetURL = URL(string: "http://maps.apple.com/?q=" + encodedAddress) {
UIApplication.shared.openURL(targetURL)
}
As suggested by matt use URLComponents
let addressPressNow = "3981 Test Drive Cupertino CA 95014"
var components = URLComponents(string: "http://maps.apple.com")
components?.queryItems = [URLQueryItem(name: "q", value: addressPressNow)]
print(components?.url)//http://maps.apple.com?q=3981%20Test%20Drive%20Cupertino%20CA%2095014
if let targetURL = components?.url {
UIApplication.shared.open(targetURL, options: [:], completionHandler: nil)
}
You are saying
NSURL(string: "http://maps.apple.com/?q=" + addressPressNow)!
Notice the exclamation mark at the end. That means "if there's a problem, crash me". You can hardly complain if you do in fact crash; that is what you asked to do.
Basically, never use NSURL(string:) if you can avoid it. To form a valid URL, build it up using URLComponents. And form it out of valid components. (It is impossible to say whether facility_address is a valid URL query, because you have not shown what it is.)
Example:
var comp = URLComponents()
comp.scheme = "https"
comp.host = "maps.apple.com"
comp.queryItems = [URLQueryItem(name: "q", value: "1 Infinite Loop, Cupertino, CA")]
if let url = comp.url {
print(url) // https://maps.apple.com?q=1%20Infinite%20Loop,%20Cupertino,%20CA
}
That gives us a valid URL that actually works.

How to Store Data once I've encoded Codable Struct to JSON

I have an array of custom Plant Objects. Plant is Codable. I use JSONEncoder().encode() to get the array encoded in JSON, but how do I store this JSON, so that it can be saved once the app closes? I remember with NSCoder I could just encode it when the app closes and use the required convenience init? to decode it, but i don't see a similar option here. Here is my Singleton Class in which I am trying to save the [Plant]
import Foundation
public class Singleton{
static let sInstance = Singleton(mPL: [Plant]())
var myPlantList: [Plant]
init(mPL: [Plant]){
self.myPlantList = mPL
}
public func savePlants(){
let jsonData = try? JSONEncoder().encode(myPlantList)
}
}
Helper extensions..
import Foundation
public extension FileManager {
// Returns a URL that points to the document folder of this project.
static var documentDirectoryURL: URL {
return try! FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
}
}
Creating a folder that will contain your data file
let documentSubdirectoryURL = URL(
fileURLWithPath: "MyFolder",
relativeTo: FileManager.documentDirectoryURL
)
try? FileManager.default.createDirectory(
at: documentSubdirectoryURL,
withIntermediateDirectories: false
)
Saving -
do {
let yourURL = URL(fileURLWithPath: "yourFileName", relativeTo: FileManager.documentDirectoryURL.appendingPathComponent("MyFolder")).appendingPathExtension("swift")
///ENCODE...
try jsonData.write(to: yourURL)
}
Decode -
do {
let yourURL = URL(fileURLWithPath: "yourFileName", relativeTo: FileManager.documentDirectoryURL.appendingPathComponent("MyFolder")).appendingPathExtension("swift")
let jsonDecoder = JSONDecoder()
let data = try Data.init(contentsOf: yourURL)
let value = try jsonDecoder.decode([Plant].self, from: data)
}
If you want to check the file containing your data, navigate to the url returned by
FileManager.documentDirectoryURL
Try
jsonData.write(to: url)

Encoding/Decoding base64 decodedData = nil

Yesterday I wrote a working code for this, but I erased it and when writing a new one something is really weird:
I encode it a picture this way:
let pictureData = UIImagePNGRepresentation(picture!)
let pictureToString64 = pictureData?.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
(I had JPEGRepresentation before, but it wasn't working, so I tried with JPEG)
and I decode this way by getting pic 64 which I believe it has the correct value. It starts with iVBORw0KGgoAAAANSUhEUgAAAUAA(...)
let decodedData = NSData(base64EncodedString: pic64, options:NSDataBase64DecodingOptions(rawValue: 0))
let decodedImage = UIImage(data: decodedData!)
let pictureDataLocal = UIImageJPEGRepresentation(decodedImage!, 100)
defaults.setObject(pictureDataLocal, forKey: "profilePicture")
The problem is that decodedData is always nil. Why is this happening?
I think this has to do with NSDataBase64DecodingOptions.
Using NSDataBase64DecodingOptions.IgnoreUnknownCharacters instead of NSDataBase64DecodingOptions(rawValue: 0), I was able to decode the Base64 encoded string back into NSData, and created a UIImage from that data:
let picture = UIImage(named: "myImage")
let pictureData = UIImagePNGRepresentation(picture!)
let pictureToString64 = pictureData?.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
let decodedData = NSData(base64EncodedString: pictureToString64!, options:NSDataBase64DecodingOptions.IgnoreUnknownCharacters)
let decodedImage = UIImage(data: decodedData!)
let pictureDataLocal = UIImageJPEGRepresentation(decodedImage!, 100)
NSDataBase64DecodingOptions inherits from OptionSetType which is why you get the rawValue initializer. It is better to use the one of the set types, rather passing in a value. I saw in the header that NSDataBase64DecodingOptions was the only public var in the struct, so I tried it.
#available(iOS 7.0, *)
public struct NSDataBase64DecodingOptions : OptionSetType {
public init(rawValue: UInt)
// Use the following option to modify the decoding algorithm so that it ignores unknown non-Base64 bytes, including line ending characters.
public static var IgnoreUnknownCharacters: NSDataBase64DecodingOptions { get }
}

Resources