User Defaults migration from Obj C to Swift - objective-c

I am working on an Update for my OS X app which was initially written in Obj-C.
The update has been re written in Swift
I am facing a strange problem of User Defaults handling.(Since User preferences must not be changed in update)
All the native type (like Bool, String) user preferences are working fine, but the the Class which was NSCoding compliant is not able to deserialse / Unarchive. It is giving an error :
Error :[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (KSPerson) for key (NS.objects); the class may be defined in source code or a library that is not linked
I made following try outs, but still not able to figure out the solution
Initially I thought that it was due to different class names(in
Swift its Person instead of KSPerson). But changing the class name (back to KSPerson) did not solve the problem
Inorder to make class more Objective C like I also tried prepending the class with #objc
Here is the code
let UD = NSUserDefaults.standardUserDefaults()
func serialize() {
// Info: personList: [KSPerson]
let encodedObject: NSData = NSKeyedArchiver.archivedDataWithRootObject(personList)
UD.setObject(encodedObject, forKey: "key1")
print("Serialization complete")
}
func deserialise() {
let encodedObject: NSData = UD.objectForKey("key1") as! NSData
let personList: [KSUrlObject] = NSKeyedUnarchiver.unarchiveObjectWithData(encodedObject) as! [KSPerson]
for person in personList {
print(person)
}
Note:
I created a duplicate copy of Objective C code, and it deserialised ( what was serialised by original copy) perfectly.
To my surprise. when I rename the class KSPerson to JSPerson in duplicate copy it gave me the exact same error as seen above
So one thing is clear. You need to have same class name to Unarchive NSCoding compliant objects. But this is clearly not sufficient for Swift

Swift classes include their module name. Objective-C classes do not. So when you unarchive the Objective-C class "KSPerson", Swift looks through all its classes and finds "mygreatapp.KSPerson" and concludes they don't match.
You need to tell the unarchiver how to map the archived names to your module's class names. You can do this centrally using setClass(_:forClassName:):
NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "KSPerson")
This is a global registration that applies to all unarchivers that don't override it. You of course can register multiple names for the same class for unarchiving purposes. For example:
NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "KSPerson")
NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "mygreatapp.Person")
NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "TheOldPersonClassName")
When you write Swift archives, by default it will include the module name. This means that if you ever change module names (or archive in one module and unarchive in another), you won't be able to unarchive the data. There are some benefits to that (it can protect against accidentally unarchiving as the wrong type), but in many cases you may not want this. If not, you can override it by registering the mapping in the other direction:
NSKeyedArchiver.setClassName("KSPerson", forClass:KSPerson.self)
This only impacts classes archived after the registration. It won't modify existing archives.
This still is pretty fragile. unarchiveObjectWithData: raises an ObjC exception when it has a problem, which Swift can't catch. You're going to crash on bad data. You can avoid that using -decodeObjectForKey rather than +unarchiveObjectWithData:
let encodedObject: NSData = UD.objectForKey("key1") as? NSData ?? NSData()
let unarchiver = NSKeyedUnarchiver(forReadingWithData: encodedObject)
let personList : [KSPerson] = unarchiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as? [KSPerson] ?? []
This returns [] if there's a problem rather than crashing. It still respects the global classname registrations.

Related

Store a custom object (class in Swift) for access by an Objective-C method

I have created a class in Swift (which I'm learning) that I use to hold certain values as follows:
#objc public class Book: NSObject {
var title: String = ""
var date: Date = Date()
var sku: Float = 0
}
I would now like to store this class in the equivalent of a property that can be accessed by my Objective-C class.
I tried creating a variable in the View Controller where the class is used as follows:
var lastBookViewed = Book()
However, when I try to save the object to the property with the followng code, I get an error:
//create instance of book
let myBook = Book()
//gather information about book
lastBookViewed = myBook //THROWS COMPILER ERROR 'Use of Unresolved Identifier lastBookViewed
Is there a way to do this?
If easier, storing the object to a property in the Objective-C file would work as well but so far I have not been able to get the Objective-C file to recognize the Book class created in Swift although I put #objc before it as recommended.
Edit
When I try to create a property or variable for storing the custom swift object in a different Swift class (the View Controller class) in the same Swift file with the following syntax:
var lastBookViewed: Book
the compiler gives a warning for the VC class "yourVC has no initializers"
Go to your project’s general settings. Select the proper target for your app. Go to “Build Settings” and switch to “All”, instead of “Basic” which is the default. Here search for the “Packaging” section. Turn on “Defines Module”, by changing “No” to “Yes”.
When this is turned on we will now be able to use swift classes inside
of objective-c files.
Before leaving the “Build Settings” look for “Product Module Name” in the “Packaging” section. This will be important so make sure to take note of and copy the “Product Module Name” exactly.
Next go to an objective-c class where you would like to be able to have access to your swift class. In the implementation or .m file of this class import a header like this:
#import "MyProjectModuleName-Swift.h"
Here the file name of the import must be the exact Project Module Name from the build settings. Any objective-c file where you want access to your swift class should include this import.
Now it is worth mentioning one potential issue that may arise when using a swift class. Objective-c cannot read top-level swift classes. So if you go to use a method or variable from your swift class directly it will not be recognized. There are one simple solution to this issue. It’s to make your class public
#objc public class myClass

What does #objc dynamic var mean in Swift 4

Could you briefly explain what #objc and dynamic mean in Swift 4 using Xcode 9.x?
With tries and errors and following articles in the stackoverflow, I have eventually achieved this snippet to work. But I would like to know a little bit about those magical keywords.
class SampleViewController: NSViewController {
#objc class Parameters : NSObject {
#objc dynamic var value1: Double = 0 // bound to Value of a NSTextfield with NumberFormatter
#objc dynamic var value2: Double = 0 // as "parameters.value1" for the Model Key Path
}
#objc dynamic var parameters = Parameters()
#objc dynamic var value3: Double { // in the similar way as "value3" for the Model Key Path
get {
return parameters.value1 + parameters.value2
}
}
override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
switch key {
case "value3" :
return Set(["parameters.value1", "parameters.value2"])
default:
return super.keyPathsForValuesAffectingValue(forKey: key)
}
}
}
Having fun with Xcode and its disassembler, I have found some. Thanks to Mike Henderson's comment.
Firstly, adding a #objc modifier seems to have the compiler write its corresponding symbol name in a __OBJC segment of executables and/or library files, which will be then used by the Objective-C run-time system.
otool -o filename command shows us the contents of __OBJC segment.
Secondly, adding a dynamic modifier seems to have the compiler insert additional assembler codes to interact with the Objective-C run-time system. The additional code realizes that accessing dynamic properties will be done through objc_msgSend() and its related functions. Similarly, calling dynamic methods also will be done through objc_msgSend().
Now, in my understandings, the jargon dynamic dispatch implies use of objc_msgSend() while static dispatch does no use of it. In the latter case, both accessing variables and calling functions will be done without intervention of the Objective-C run-time system, which is in the similar, but not exactly same, way of C++ ABI.
Apparently, static one is faster than dynamic one. But static one is incapable of Objective-C's magical benefits, though. With the programming language Swift, we have opportunities to utilize both aspects by choosing either static or dynamic dispatch depending on the situation, by omitting or adding those magical keywords, respectively.
Thanks!
Further readings:
Objective-C Runtime
Using Swift with Cocoa and Objective-C (Swift 4.0.3)
#objc means you want your Swift code (class, method, property, etc.) to be visible from Objective-C.
dynamic means you want to use Objective-C dynamic dispatch.
Swift 3 - dynamic vs #objc

Swift class properties not initialized when constructed by Objective C code

I'm attempting to create a class in Swift 3 to implement a Cordova plugin. I have this building and running, but the application crashes whenever any properties of the class are accessed. I've tried two ways of initializing the class:
#objc(DSFMediaCentre)
class DSFMediaCentre : CDVPlugin
{
var players = [UUID:DSFPlayerHandler] ();
...
}
and
#objc(DSFMediaCentre)
class DSFMediaCentre : CDVPlugin
{
var players :[UUID:DSFPlayerHandler];
override init () {
players = [:];
}
...
}
However, when my players property is used, the result is a EXC_BAD_ACCESS exception, with an address that looks like a null pointer dereference.
The object is being created by Objective C code, which is a language I have no familiarity with at all, but I think this is the line that creates it:
obj = [[NSClassFromString(className)alloc] initWithWebViewEngine:_webViewEngine];
The CDVPlugin class contains a comment stating that initWithWebViewEngine should not be overridden (and indeed I do not seem to be able to override this method, because while it is declared in the CDVPlugin.m file, it isn't mentioned in CDVPlugin.h, so the Swift compiler doesn't seem to know about it), but rather initialization code should be placed in a method called pluginInitialize instead. However, if I do that I get a compiler error ("Class DSFMediaCentre has no initializers").
Furthermore, if I put my init() method back in and set it to call pluginInitialize(), like this:
override init () {
super.init(); // necessary otherwise next line is an error
pluginInitialize();
}
override func pluginInitialize() {
players = [:];
}
the error then changes to "Property 'self.players' not initialized at super.init call".
How do I make this class initialize correctly?
You have a mismatch between the strict initialization system required by the language and the procedure used by the framework you're working with.
Swift demands that a) properties be initialized as part of object construction, and b) that construction be chained to the type's supertype. But the CDVPlugin type is doing the construction on your behalf; you don't have the ability to customize it. (This makes more sense in ObjC, because it doesn't have the same compile-time restrictions as Swift.)
The situation is similar to unpacking an object from a nib file. In that case too, because it's the nib loading system that's constructing your object, you don't have the ability to customize the initializer. Your type will always be constructed by init(coder:). In a certain sense, your initialization point moves further down, to awakeFromNib(), and among other things, that forces outlets to other objects in the archive to be declared as optional, usually implicitly unwrapped.
The same solution should avail you here. You should consider pluginInitialize() to be your initialization point. The language then requires that properties be optional, since they are not filled at its initialization point. Therefore, make the property an IUO:
#objc(DSFMediaCentre)
class DSFMediaCentre : CDVPlugin
{
var players :[UUID:DSFPlayerHandler]!
override func pluginInitialize() {
players = [:];
}
}
and all should be well.
The other solution is to use lazy keyword
lazy var players :[UUID:DSFPlayerHandler] = [:]
So, you don't need to initialize players in initializer but still make sure players always non-nulable

Error accessing function of class while converting Obj C project to Swift

I added my swift class to the target while removing my header file of the same objective C class from the target but this error shows when I try and build my project. I can't attach an image right now but the error states: "Use of instance member 'url' on type 'ServerURLFactory'; did you mean to use a value of type 'ServerURLFactory' instead?"
let accessURL: NSURL = NSURL(string: "\(ServerURLFactory.url())/CygnetInstanceXMLServlet?cygnetId=\(idNumber)")!
print(accessURL)
Has anyone ran into a similar problem and how to fix this confusing bug? Its as if the program is still trying to call the Obj C function instead of explicitly calling the one in the Swift file.
You're calling .url() on ServerURLFactory itself as a type:
ServerURLFactory.url()
I guess you should instantiate the class first. Probably something like this, but it depends on how the class is implemented:
let factory = ServerURLFactory()
Then:
factory.url()

How to change the namespace of a Swift class?

When you implement a class MyGreatClass in Swift its fully qualified name will by <MyPackageName>.MyGreatClass. This is different to Objective-C, where the fully qualified name of that same class is MyGreatClass.
Unfortunately this introduces a problem for me. When I am using NSUnarchiver and the archive was written with Objective-C objects I cannot unpack it with Swift-classes(see below for a detailed description).
This means I need to find a way to rename the namespace for my Swift classes. How do I do that?
Any help would be great!
Background: Why can't NSUnarchiver see/load my swift class?
I have implemented a small program to read a file, which was archived with NSArchive.
These are my files:
main.swift:
import Foundation
// parse command line - total path to *.trace file (from Apple's Instruments app)
var traceFilePath = Process.arguments[1]
var traceFile = NSURL(fileURLWithPath: traceFilePath)
var error:NSError?
// check if the file exists
if (traceFile?.checkResourceIsReachableAndReturnError(&error) == false){
// file does not exist or cannot be accessed
println("\(error)")
exit(1)
}
var rawData = NSData(contentsOfURL: traceFile!)
var data = NSUnarchiver(forReadingWithData: rawData!)
var decodedObject: AnyObject? = data?.decodeObject()
XRObjectAllocRun.swift:
import Foundation
class XRObjectAllocRun: NSObject {
// class to be implemented
}
When I now run my application on an Instruments-file I am getting the following error: Terminating app due to uncaught exception 'NSArchiverArchiveInconsistency', reason: '*** class error for 'XRObjectAllocRun': class not loaded'.
This is really strange because when I add the exact same class in an Objective-C file with a bridging header file I have no issues.
trace file reader-Bridging-Header.h: is empty.
XRObjectAllocRun.h:
#import <Foundation/Foundation.h>
#interface XRObjectAllocRun : NSObject
#end
XRObjectAllocRun.m:
#import "XRObjectAllocRun.h"
#implementation XRObjectAllocRun
#end
What am I missing? Why is my Objective-C class found, whereas my Swift class is not?
Swift has no issues for example with var x = XRObjectAllocRun() in main.swift, but yet the NSUnarchiver still complaints about a missing XRObjectAllocRun class when I stay purely within Swift. Is the NSUnarchiver looking in the wrong places - does it for some reason only accept Objective-C classes?
If you want to know what I am trying to do check this stackoverflow question out.
Update
This is what apple writes:
Swift classes are namespaced based on the module they are compiled in, even when used from Objective-C code. Unlike Objective-C, where all classes are part of a global namespace
Further more:
For example, when you create a document–based Mac app, you provide the name of your NSDocument subclass in your app’s Info.plist file. In Swift, you must use the full name of your document subclass, including the module name derived from the name of your app or framework.
Yikes, trying to figure out the mess now...
Try this when you declare your class:
#objc(XRObjectAllocRun) class XRObjectAllocRun: NSObject {
// class to be implemented
}
That will give this class the same name as the archived class, namely XRObjectAllocRun, instead of the namespaced Swift name trace_file_reader.XRObjectAllocRun.
This is always a concern when you're translating from Objective-C to Swift and you've got an existing archive to deal with. See Apple's documentation:
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html
Note the discussion under "Exposing Swift Interfaces in Objective-C".