Swift init from unknown class which conforms to protocol - ios

I'm currently working on updating a large project from Objective-C to Swift and I'm stumped on how to mimic some logic. Basically we have a class with a protocol which defines a couple functions to turn any class into a JSON representation of itself.
That protocol looks like this:
#define kJsonSupport #"_jsonCompatible"
#define kJsonClass #"_jsonClass"
#protocol JsonProtocol <NSObject>
- (NSDictionary*)convertToJSON;
- (id)initWithJSON:(NSDictionary* json);
#end
I've adapted that to Swift like this
let JSON_SUPPORT = "_jsonCompatible"
let JSON_CLASS = "_jsonClass"
protocol JsonProtocol
{
func convertToJSON() -> NSDictionary
init(json: NSDictionary)
}
One of the functions in the ObjC class runs the convertToJSON function for each object in an NSDictionary which conforms to the protocol, and another does the reverse, creating an instance of the object with the init function. The output dictionary also contains two keys, one denoting that the dictionary in question supports this protocol (kJsonSupport: BOOL), and another containing the NSString representation of the class the object was converted from (kJsonClass: NSString). The reverse function then uses both of these to determine what class the object was converted from to init a new instance from the given dictionary.
All of the classes are anonymous to the function itself. All we know is each class conforms to the protocol, so we can call our custom init function on it.
Here's what it looks like in ObjC:
Class rootClass = NSClassFromString(obj[kJsonClass]);
if([rootClass conformsToProtocol:#protocol(JsonProtocol)])
{
Class<JsonProtocol> jsonableClass = (Class<JsonProtocol>)rootClass;
[arr addObject:[[((Class)jsonableClass) alloc] initWithJSON:obj]];
}
However, I'm not sure how to make this behavior in Swift.
Here's my best attempt. I used Swiftify to try and help me get there, but the compiler isn't happy with it either:
let rootClass : AnyClass? = NSClassFromString(obj[JSON_CLASS] as! String)
if let _rootJsonClass = rootClass as? JsonProtocol
{
weak var jsonClass = _rootJsonClass as? AnyClass & JsonProtocol
arr.add(jsonClass.init(json: obj))
}
I get several errors on both the weak var line and the arr.add line, such as:
Non-protocol, non-class type 'AnyClass' (aka 'AnyObject.Type') cannot be used within a protocol-constrained type
'init' is a member of the type; use 'type(of: ...)' to initialize a new object of the same dynamic type
Argument type 'NSDictionary' does not conform to expected type 'JsonProtocol'
Extraneous argument label 'json:' in call
Is there any way for me to instantiate from an unknown class which conforms to a protocol using a custom protocol init function?

You will likely want to rethink this code in the future, to follow more Swift-like patterns, but it's not that complicated to convert, and I'm sure you have a lot of existing code that relies on behaving the same way.
The most important thing is that all the objects must be #objc classes. They can't be structs, and they must subclass from NSObject. This is the major reason you'd want to change this to a more Swifty solution based on Codable.
You also need to explicitly name you types. Swift adds the module name to its type names, which tends to break this kind of dynamic system. If you had a type Person, you would want to declare it:
#objc(Person) // <=== This is the important part
class Person: NSObject {
required init(json: NSDictionary) { ... }
}
extension Person: JsonProtocol {
func convertToJSON() -> NSDictionary { ... }
}
This makes sure the name of the class is Person (like it would be in ObjC) and not MyGreatApp.Person (which is what it normally would be in Swift).
With that, in Swift, this code would be written this way:
if let className = obj[JSON_CLASS] as? String,
let jsonClass = NSClassFromString(className) as? JsonProtocol.Type {
arr.add(jsonClass.init(json: obj))
}
The key piece you were missing is as? JsonProtocol.Type. That's serving a similar function to +conformsToProtocol: plus the cast. The .Type indicates that this is a metatype check on Person.self rather than a normal type check on Person. For more on that see Metatype Type in the Swift Language Reference.
Note that the original ObjC code is a bit dangerous. The -initWithJSON must return an object. It cannot return nil, or this code will crash at the addObject call. That means that implementing JsonProtocol requires that the object construct something even if the JSON it is passed is invalid. Swift will enforce this, but ObjC does not, so you should think carefully about what should happen if the input is corrupted. I would be very tempted to change the init to an failable or throwing initializer if you can make that work with your current code.
I also suggest replacing NSDictionary and NSArray with Dictionary and Array. That should be fairly straightforward without redesigning your code.

Related

Mutable Swift array becomes NSArray when I initialize with Obj-C

I'm creating my base models in Swift(2.0) and then controlling the views in Objective-C. I'm still new to Swift, so hopefully I'm just overlooking something simple, but here is the problem:
I’m making a mutable array in Swift, but when I initialize the array in my Objective-c portion of the program, it becomes an NSArray, more specifically it becomes: Swift._SwiftDeferredNSArray
Why is it becoming immutable when I initialize? Here’s my Swift code:
import Foundation
#objc public class Model : NSObject {
var books:[Book]
override init(){
self.books = [Book]()
}
}
And here’s my Obj-c Code;
Model *bookCollection = [[Model alloc]init];
I’m unable to add objects to my bookCollection.books array (because it has become an NSArray) and when I set a breakpoint and po it, I can see that it is a Swift._SwiftDeferredNSArray. bookCollection.books is supposed to be an NSMutableArray.
Any thoughts?
In swift, the difference between mutable and immutable array is;
var books:[Book] // is a mutable array
let books:[Book] = [book1, book2]; // is immutable array due to let
but I don't think, same rule is followed when bridging to ObjC.
Just for a fix, you may have mutableArray specifically.
import Foundation
#objc public class Model : NSObject {
var books:NSMutableArray = NSMutableArray();
override init(){
super.init();
// other code
}
}
You will need to parse the values to Book Class when retrieving from the array.
bookCollection.books is supposed to be an NSMutableArray.
No, it is not. Var does not mean that the bridged Objective-C object is to be mutable: it means that the property can be assigned to.
The Swift array type is a structure, not a class. This has important bridging implications. The reference itself cannot be shared without passing it as an inout value, and even then the references cannot be stored. If it bridged as a NSMutableArray, it would be possible to have undetectable mutating references, and Swift does not allow that.
You should be able to assign a new NSArray to your property from Objective-C code, though. For instance, this should work:
bookCollection.books = [bookCollection.books arrayByAddingObject:myNewBook];
Your other option, obviously, is to declare books as a NSMutableArray from the Swift side.

Swift dynamic variable can't be of type Printable

I have a Swift project that contains two UITableViewControllers. The second UITableViewController is linked to a MVC model called Model. According to the UITableViewCell I select in the first UITableViewController, I want to initialize some properties of Model with Ints or Strings. Therefore, I've decided to define those properties with Printable protocol type. In the same time, I want to perform Key Value Observing on one of these properties.
Right now, Model looks like this:
class Model: NSObject {
let title: String
let array: [Printable]
dynamic var selectedValue: Printable //error message
init(title: String, array: [Printable], selectedValue: Printable) {
self.title = title
self.array = array
self.selectedValue = selectedValue
}
}
The problem here is that the following error message appears on the selectedValue declaration line:
Property cannot be marked dynamic because its type cannot be
represented in Objective-C
If I go to the Xcode Issue Navigator, I can also read the following line:
Protocol 'Printable' is not '#objc'
Is there any workaround?
There is no way to do what you want. Non-#objc protocols cannot be represented in Objective-C. One reason is that Non-#objc protocols can represent non-class types (and indeed, you said that you wanted to use it for Int and String, both non-class types), and protocols in Objective-C are only for objects.
KVO is a feature designed for Objective-C, so you must think about what you expect it to see from the perspective of Objective-C. If you were doing this in Objective-C, you would not want to have a property that could either be an object like id or a non-object like int -- you can't even declare that. Instead, as you said in your comment, you probably want it to be just objects. And you want to be able to use Foundation's bridging to turn Int into NSNumber * and String into NSString *. These are regular Cocoa classes that inherit from NSObject, which implements Printable.
So it seems to me you should just use NSObject or NSObjectProtocol.
Unfortunately ObjC does not treat protocols as types, they are just a convenient way of grouping members. Under the covers they are of type Any, so regretfully you will have to make the property Any and cast to Printable.
The best I can thing of is:
dynamic var selectedValue: Any
var printableValue : Printable {
get {
return (Printable)selectedValue
}
set {
selectedValue = newValue
}
}

How to define an array of objects conforming to a protocol?

Given:
protocol MyProtocol {
typealias T
var abc: T { get }
}
And a class that implements MyProtocol:
class XYZ: MyProtocol {
typealias T = SomeObject
var abc: T { /* Implementation */ }
}
How can I define an array of objects conforming to MyProtocol?
var list = [MyProtocol]()
Gives (together with a ton of SourceKit crashes) the following error:
Protocol 'MyProtocol' can only be used as a generic constraint because it has Self or associated type requirements
Even though the typealias is in fact defined in MyProtocol.
Is there a way to have a list of object conforming to a protocol AND having a generic constraint?
The problem is about using the generics counterpart for protocols, type aliases.
It sounds weird, but if you define a type alias, you cannot use the protocol as a type, which means you cannot declare a variable of that protocol type, a function parameter, etc. And you cannot use it as the generic object of an array.
As the error say, the only usage you can make of it is as a generic constraint (like in class Test<T:ProtocolWithAlias>).
To prove that, just remove the typealias from your protocol (note, this is just to prove, it's not a solution):
protocol MyProtocol {
var abc: Int { get }
}
and modify the rest of your sample code accordingly:
class XYZ: MyProtocol {
var abc: Int { return 32 }
}
var list = [MyProtocol]()
You'll notice that it works.
You are probably more interested in how to solve this problem. I can't think of any elegant solution, just the following 2:
remove the typealias from the protocol and replace T with AnyObject (ugly solution!!)
turn the protocol into a class (but that's not a solution that works in all cases)
but as you may argue, I don't like any of them. The only suggestion I can provide is to rethink of your design and figure out if you can use a different way (i.e. not using typealiased protocol) to achieve the same result.

Swift func that takes a Metatype?

As Apple says in the Metatype Type section in Swift's docs:
A metatype type refers to the type of any type, including class types, structure types, enumeration types, and protocol types.
Is there a base class to refer to any class, struct, enum, or protocol (eg MetaType)?
My understanding is that protocol types are limited to use as a generic constraint, because of Self or associated type requirements (well, this is what an Xcode error was telling me).
So, with that in mind, maybe there is a Class base class for identifying class references? Or a Type base class for all constructable types (class, struct, enum)? Other possibilities could be Protocol, Struct, Enum, and Closure.
See this example if you don't get what I mean yet.
func funcWithType (type: Type) {
// I could store this Type reference in an ivar,
// as an associated type on a per-instance level.
// (if this func was in a class, of course)
self.instanceType = type
}
funcWithType(String.self)
funcWithType(CGRect.self)
While generics work great with 1-2 constant associated types, I wouldn't mind being able to treat associated types as instance variables.
Thanks for any advice!
This works:
func funcWithType (type: Any.Type) {
}
funcWithType(String.self)
funcWithType(CGRect.self)
Given your example an implementation would be:
// protocol that requires an initializer so you can later call init from the type
protocol Initializeable {
init()
}
func funcWithType (type: Initializeable.Type) {
// make a new instance of the type
let instanceType = type()
// in Swift 2 you have to explicitly call the initializer:
let instanceType = type.init()
// in addition you can call any static method or variable of the type (in this case nothing because Initializeable doesn't declare any)
}
// make String and CGRect conform to the protocol
extension String: Initializeable {}
extension CGRect: Initializeable {}
funcWithType(String.self)
funcWithType(CGRect.self)

swift: Equivalent objective-c runtime class

What is equivalent swift code for below Objective-C code. I couldn't find swift topic with runtime concept.
#import <objc/runtime.h>
Class class = [self class];
Trying to get class object of self?
Update:
Tried with below code, got error as 'UIViewController.type' doesn't conform to protocol 'AnyObject'
var klass: AnyClass = object_getClass(self)
Note: Found this post, but wouldn't helped.
First, it's hard to translate that code to Swift without knowing what you used that class object for in Objective-C.
In Objective-C, class objects are objects, and the type Class can hold a pointer to any class object. However, when Objective-C APIs are bridged to Swift, the type Class is converted to AnyClass! in Swift, where AnyClass is defined as AnyObject.Type. Types in Swift are not objects, and thus are not directly equivalent to class objects in Objective-C. However, if you intend to use an Objective-C API from Swift, it will have been bridged to expect AnyClass anyway, so you have to pass a type. You can get the type of any expression using .dynamicType; for example:
self.dynamicType
(If you really want to get the class object as an Swift object the same way as in Objective-C, and not as a Swift type, there are some convoluted ways to do that too.)
However, your description of your problem reveals another issue. If you just want to get the type of an object, and self is an object, then var klass: AnyClass = object_getClass(self) should have worked, since object_getClass() takes an AnyObject and returns an AnyClass. The only explanation for it not working is if self is not an object. Your error message reveals that, indeed, self is a type, not an object.
self is a type if this code is running in a class method. You should have really given context for your code (obviously, you didn't put Class class = [self class]; at the top level of a file), because taken out of context it's easy to misunderstand. In Objective-C Cocoa, there are two very different methods named class: an instance method, -class, which returns the class of the object, and a class method, +class, which simply returns the (class) object it's called on. Since your code is in a class method, in Objective-C, self points to a class object, and [self class] runs the class method +class, which just returns the object it's called on. In other words, [self class] is exactly identical to self. You should have just written self all along, but didn't realize it.
So the answer is that the Objective-C should have been
Class class = self;
and similarly the Swift should be
var klass: AnyClass = self
In Swift 3, self.dynamicType (and dynamicType in general) has been removed.
You now use:
type(of: self)
var klass: AnyClass = object_getClass(self)
NSStringFromClass(klass)

Resources