Error archiving CMTime with NSKeyedArchiver in swift4.0 in ios9.3 - swift4

The following code archivedTimes() builds successfully in swift4.
And it runs fine on a device with ios10.3 installed.
typealias Time = CMTime
typealias Times = [Time]
static let times: Times = Array<Int64>.init(1...9).map({ CMTime.init(value: $0, timescale: 100) })
static func archivedTimes() -> Data {
return archived(times: times)
}
static func archived(times: Times) -> Data {
let values = times.map({ NSValue.init(time: $0) })
return NSKeyedArchiver.archivedData(withRootObject: values) // ERROR here
// -- ideally would instead be:
// return NSKeyedArchiver.archivedData(withRootObject: times)
// -- but probably not compatible with ios 9.3
}
However, while running it on a device with ios9.3 installed, it crashes saying:
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '*** -[NSKeyedArchiver
encodeValueOfObjCType:at:]: this archiver cannot encode structs'
My guess is that it may have something to do with some conflict between the new Codable protocol and the old NSCoder protocol. But I don't know what!
Note that the issue has nothing to do with the array. As archiving a simple CMTime also leads to such error. However, I posted it like this, because archiving the array of CMTime is ultimately my objective.

I believe Codable protocol is only available in ios10, therefore on ios9, CMTime does not implement Codable.
So for ios9, I went with a wrapper class for a CMTime, which implements the NSCoding protocol.
This can be done by importing AVFoundation which declares both the extension to NSValue and to NSCoder so as to encode CMTime.
So then I went with an array of WrappedTime.init($0), instead of an array of NSValue.init(time: $0).
class WrappedTime: NSObject, NSCoding {
enum EncodeKey: String {
case time = "time"
}
let time: CMTime
// ...
func encode(with aCoder: NSCoder) {
aCoder.encode(time, forKey: EncodeKey.time.rawValue)
}
required init?(coder aDecoder: NSCoder) {
time = aDecoder.decodeTime(forKey: EncodeKey.time.rawValue)
}
init(_ time: Time) {
self.time = time
}
}

Related

How to properly copy CAShapeLayer in swift?

I'm trying to create copies of CAShapeLayer in swift but I'm getting a crash
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CAShapeLayer copyWithZone:]: unrecognized selector sent to instance 0x282e87e60'
which extra steps should I take to allow CALayer .copy() work without crashing the app?
Even if I don't cast the result of .copy() it stills fails exactly at that copy() line...
private var drawLines: [CAShapeLayer]
func getCopiedLayers() -> [CAShapeLayer] {
return drawLines.compactMap { layer -> CAShapeLayer? in
return layer.copy() as? CAShapeLayer
}
}
what I'm doing wrong here?
Thanks in advance for the answers
CALayer does not conform to NSCopying from API, but it conforms to NSSecureCoding, so it is possible to add copying capability as below
Tested with Xcode 11.2 / iOS 13.2 (with CAShapeLayer &
CAGradientLayer)
extension CALayer : NSCopying {
public func copy(with zone: NSZone? = nil) -> Any {
if let data = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) {
if let newInstance = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) {
return newInstance
}
}
fatalError() // << should never got here
}
}
Now, it is possible to call layer.copy() to any layer (theoretically) without exception.

Xcode - Access persistent data from simulator (NSCoder)

I am storing persistent data in my application using NSCoder like below:
class UserNotification: NSObject, NSCoding {
var message: String!
init(message: String) {
self.message = message
}
required convenience init(coder aDecoder: NSCoder) {
let message = aDecoder.decodeObject(forKey: "message") as! String
self.init(message: message)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(message, forKey: "message")
}
}
This is how they are loaded into the application:
let defaults = UserDefaults.standard
if (defaults.object(forKey: "userNotifications") != nil) {
let decoded = defaults.object(forKey: "userNotifications") as! Data
let decodedUserNotifications = NSKeyedUnarchiver.unarchiveObject(with: decoded as Data) as! [UserNotification]
userNotifications = decodedUserNotifications
}
I also have included a way for the user to clear all the saved data. For clearing, I have implemented:
let newListData = NSKeyedArchiver.archivedData(withRootObject: userNotifications)
let defaults = UserDefaults.standard
defaults.set(newListData, forKey: "userNotifications")
defaults.synchronize()
All user notification objects are stored in a globally accessible array:
userNotifications = [UserNotification]()
I would like to make sure that all the persistent data is removed once the user has cleared them. I was looking through the folder of my simulator (~/Library/Developer/CoreSimulator/Devices) in order to find the persistent data and to see if it gets removed, but I have not managed to find it anywhere.
Everything seems to work when running the application (saving data, erasing it etc.) but I would like to make sure that all data get erased correctly on the device. There are some thumbnails being saved and therefore I want to make sure it really becomes erased and not taking up unnecessary space.
So my question is where the data is stored when using NSCoder? I am able to find locally stored images from using FileManager, but the encoded data is nowhere to be found.

How to use the delegates with NSKeyedUnarchiver?

I am using NSKeyedUnarchiver to unarchive an object and would like to use the delegates (NSKeyedUnarchiverDelegate), but my delegates are not called. Archiving and Unarchiving is working fine, but the Delegates (unarchiver & unarchiverDidFinish) are not called. Can someone help?
I have the following implementation:
class BlobHandler: NSObject , NSKeyedUnarchiverDelegate{
func load() -> MYOBJECTCLASS{
let data:NSData? = getBlob();
var mykeyedunarchiver:NSKeyedUnarchiver=NSKeyedUnarchiver(forReadingWithData: data!);
mykeyedunarchiver.delegate = self;
let temp=mykeyedunarchiver.decodeObjectForKey("rootobject")
// No delegates are called
if temp==nil {
blobsexists=false;
}else{
objectreturn = temp! as! MYOBJECTCLASS;
return objectreturn;
}
}
func save1(myobject:MYOBJECTCLASS){
let data = NSMutableData()
var keyedarchiver:NSKeyedArchiver=NSKeyedArchiver(forWritingWithMutableData: data);
keyedarchiver.encodeObject(maptheme, forKey: "rootobject");
let bytes = data.bytes;
let len=data.length;
saveblob(bytes);
}
The following delegates, which are also implemented in my Blobhandler, are never called:
func unarchiver(unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? {
print("I am in unarchiver !");
return nil;
}
func unarchiverDidFinish(_ unarchiver: NSKeyedUnarchiver){
print("I am in unarchiverDidFinish ! ");
}
I don't know what it was, but its working after a clean and rebuild of the project.
I notice with different cases, that the builds are not in sync sometimes. There is sometimes code, which is in XCode but it is not executed. Sounds unbelievable, but I guess its true.
XCode 7.2
I think the first function is never called since you didn't actually feed a "cannotDecodeObjectOfClassName" at all, since you only did try to unarchive previously archived data. You can try this method(or something requires a class name) to validate your solution(feed a class doesn't conform NSCoding):
unarchiver.decodeObjectOfClass(cls: NSCoding.Protocol, forKey: String)
The second one is a little bit tricky. I've tried this method in a similar situation and it turned out that unarchiverDidFinish only get called when a complete unarchiving job is done and probably before it's destroyed. For example, I had a NSCoding class and the convenience initiator is like
required convenience init?(coder aDecoder: NSCoder) {
let unarchiver = aDecoder as! NSKeyedUnarchiver
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
unarchiver.delegate = appDelegate.uad
let name = unarchiver.decodeObjectForKey(PropertyKey.nameKey) as! String
print(321)
self.init(name: name, photo: photo, rating: rating)
}
uad is an instance of class:
class UAD:NSObject, NSKeyedUnarchiverDelegate {
func unarchiverDidFinish(unarchiver: NSKeyedUnarchiver) {
print(123)
}
}
And in the view controller the loading process is like
func load() -> [User]? {
print(1)
let ret = NSKeyedUnarchiver.unarchiveObjectWithFile(ArchiveURL.path!) as? [User]
print(2)
return ret
}
And the output is like:
1
321
321
321
321
321
123
2
After finishing loading a group of users, the unarchiverDidFinish finally got called once. Notice that this is a class function and an anonymous instance is created to finish this sentence:
NSKeyedUnarchiver.unarchiveObjectWithFile(ArchiveURL.path!) as? [User]
So I really believe that this function only get called before it is destroyed or a group of call back functions is finished.
I am not quite sure if this is the case for you. You may try to make your unarchiver object global and destroy it after your loading is done to see whether this function is called.
Correct me if anything not right.
To make either unarchiverWillFinish: and unarchiverDidFinish: be called properly, we have to invoke finishDecoding when finished decoding.
Once you have the configured decoder object, to decode an object or data item, use the decodeObjectForKey: method. When finished decoding a keyed archive, you should invoke finishDecoding before releasing the unarchiver.
We notify the delegate of the instance of NSKeyedUnarchiver and perform any final operations on the archive through invoking this method. And once this method is invoked, according to Apple's official documentation, our unarchiver cannot decode any further values. We would get following message if we continue to perform any decoding operation after invoked finishDecoding:
*** -[NSKeyedUnarchiver decodeObjectForKey:]: unarchive already finished, cannot decode anything more
It also makes sense for encoding counterparts.

Handling NSCoding Errors in Swift

How should errors related to NSCoding be handled in Swift?
When an object is initialized using init?(coder:) it may fail to be initialized if the data is invalid. I'd like to catch these errors and appropriately handle them. Why is init?(coder:) not defined as a throwing function in Swift?
NSCoding defines it as Optional:
init?(coder aDecoder: NSCoder)
So it is certainly possible to detect errors.
90% of "why does Swift...?" questions can be answered with "because Cocoa." Cocoa does not define initWithCoder: as returning an error, so it does not translate to throws in Swift. There would be no way to cleanly bridge it to existing code. (NSCoding goes back NeXTSTEP. We've built a lot of software without returning an NSError there. Doesn't mean it might not be nice sometimes, but "couldn't init" has traditionally been enough.)
Check for nil. That means that something failed. That is all the information that is provided.
I've never in practice had to check too deeply that the entire object graph was correct. If it isn't, you're incredibly likely to get other errors anyway, and remember that NSKeyedUnarchiver will raise an ObjC exception (!!!) if it fails to decode. Unless you wrap this in an ObjC #catch, you're going to crash anyway. (And yes, that's pretty crazy, but still true.)
But if I wanted to be extremely careful and make sure that things I expected to be in the archive were really in the archive (even if they were nil), I might do it this way (untested; it compiles but I haven't made sure it really works):
import Foundation
enum DecodeError: ErrorType {
case MissingProperty(String)
case MalformedProperty(String)
}
extension NSCoder {
func encodeOptionalObject(obj: AnyObject?, forKey key: String) {
let data = obj.map{ NSKeyedArchiver.archivedDataWithRootObject($0) } ?? NSData()
self.encodeObject(data, forKey: key)
}
func decodeRequiredOptionalObjectForKey(key: String) throws -> AnyObject? {
guard let any = self.decodeObjectForKey(key) else {
throw DecodeError.MissingProperty(key)
}
guard let data = any as? NSData else {
throw DecodeError.MalformedProperty(key)
}
if data.length == 0 {
return nil // Found nil
}
// But remember, this will raise an ObjC exception if it's malformed data!
guard let prop = NSKeyedUnarchiver.unarchiveObjectWithData(data) else {
throw DecodeError.MalformedProperty(key)
}
return prop
}
}
class MyClass: NSObject, NSCoding {
static let propertyKey = "property"
let property: String?
init(property: String?) {
self.property = property
}
required init?(coder aDecoder: NSCoder) {
do {
property = try aDecoder.decodeRequiredOptionalObjectForKey(MyClass.propertyKey) as? String
} catch {
// do something with error if you want
property = nil
super.init()
return nil
}
super.init()
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeOptionalObject(property, forKey: MyClass.propertyKey)
}
}
As I said, I've never actually done this in a Cocoa program. If anything were really corrupted in the archive, you're almost certainly going to wind up raising an ObjC exception, so all this error checking is likely overkill. But it does let you distinguish between "nil" and "not in the archive." I just encode the property individually as an NSData and then encode the NSData. If it's nil, I encode an empty NSData.

App Delegate weird error when trying to add element to NSUserDefaults

I've got a really weird error while running my app on Xcode 7 (Swift 2) that shows a "Thread 1: signal SIGABRT" running error message in the App Delegate class of my app. However I've actually already got this "Thread 1: signal SIGABRT" running error message in the App Delegate class lots of times, mainly when deleting an outlet reference in my code and forgetting to also delete it from storyboard. But that's certainly the first time I've got this same error when trying to make the command:
let wasteGain = WastesGainsClass(value: enteredMoney, originOrCat: segControlArray[segControl.selectedSegmentIndex], specification: plusEspecTField.text!, date: dateArray, mode: "gain")
gains.append(wasteGain)
NSUserDefaults.standardUserDefaults().setObject(gains, forKey: "gains")
What happens is that if I just comment the line NSUserDefaults.standardUserDefaults().setObject(gains, forKey: "gains") the app doesn't crash! So the error might just be in that line.
If anyone could help me, I`d thank you so much.
PS: WastesGainsClass format is like this:
class WastesGainsClass {
var value:Int = 0
var origin:String
var specification:String
var date:[String]
var mode:String
var rowMode:Int = 0
init(value:Int, originOrCat:String, specification:String, date:[String], mode:String) {
self.value = value
self.origin = originOrCat
self.specification = specification
self.date = date
self.mode = mode
}
}
From documentation:
The NSUserDefaults class provides convenience methods for accessing
common types such as floats, doubles, integers, Booleans, and URLs. A
default object must be a property list, that is, an instance of (or
for collections a combination of instances of): NSData, NSString,
NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any
other type of object, you should typically archive it to create an
instance of NSData.
In Swift you can also use:
Int, UInt, Double, Float and Bool types because they are automatically bridged to NSNumber;
String bridged to NSString
[AnyObject] because it is bridged to NSArray;
[NSObject: AnyObject] because it is bridged to NSDictionary.
Of course type of array elements and dictionary values must be one of above types. Dictionary key type must be NSString (or bridged String).
To store instances of any other class you have two options:
Your custom class must be subclass of NSObject and conform to
NSCoding protocol and then you can archive object of this class to NSData with NSKeyedArchiver.archivedDataWithRootObject() and save it to NSUserDefaults and later retrieve it from NSUserDefaults and unarchive with NSKeyedUnarchiver.unarchiveObjectWithData():
import Foundation
class WastesGainsClass: NSObject, NSCoding {
var value: Int
init(value: Int) {
self.value = value
}
required init(coder aDecoder: NSCoder) {
value = aDecoder.decodeObjectForKey("value") as! Int
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(value, forKey: "value")
}
}
var gains = [WastesGainsClass(value: 1), WastesGainsClass(value: 2)]
NSUserDefaults.standardUserDefaults().setObject(gains.map { NSKeyedArchiver.archivedDataWithRootObject($0) }, forKey: "gains")
if let gainsData = NSUserDefaults.standardUserDefaults().objectForKey("gains") as? [NSData] {
gains = gainsData.map { NSKeyedUnarchiver.unarchiveObjectWithData($0) as! WastesGainsClass }
}
You can save your custom object properties to dictionary and store that
dictionary in NSUserDefaults:
import Foundation
class WastesGainsClass {
var value: Int
init(value: Int) {
self.value = value
}
}
extension WastesGainsClass {
convenience init(dict: [NSObject: AnyObject]) {
self.init(value: dict["value"] as? Int ?? 0)
}
func toDict() -> [NSObject: AnyObject] {
var d = [NSObject: AnyObject]()
d["value"] = value
return d
}
}
var gains = [WastesGainsClass(value: 1), WastesGainsClass(value: 2)]
NSUserDefaults.standardUserDefaults().setObject(gains.map { $0.toDict() }, forKey: "gains")
if let dicts = NSUserDefaults.standardUserDefaults().objectForKey("gains") as? [[NSObject: AnyObject]] {
gains = dicts.map { WastesGainsClass(dict: $0) }
}
NSUserDefaults unfortunately can't accept arbitrary objects, only objects that can be encoded in a Property List. See Apple's reference guide for Property Lists to learn which objects can be stored.
If you need to save several WastesGainsClass objects, you may wish to write a method that returns a Dictionary encoding their Property List-representable properties, and an initializer that accepts such a Dictionary to restore the object.
However, if you truly need to save multiple custom objects like this, you probably don't want to use NSUserDefaults at all. Consider a document-based app, and look into NSCoding.
The code you posted tries to save an array of custom objects to NSUserDefaults. You can't do that. Implementing the NSCoding methods doesn't help. You can only store things like NSArray, NSDictionary, NSString, NSData, NSNumber, and NSDate in NSUserDefaults.
You need to convert the object to NSData (like you have in some of the code) and store that NSData in NSUserDefaults. You can even store an NSArray of NSData if you need to.
see this post : Attempt to set a non-property-list object as an NSUserDefaults