Jasonette / JASONETTE-iOS

📡 Native App over HTTP, on iOS
https://www.jasonette.com
MIT License
5.26k stars 352 forks source link

Extending • Swift-class component returns nil #363

Closed RomanSlyepko closed 5 years ago

RomanSlyepko commented 5 years ago

Extending with custom components doesn't work for Swift classes. There is an issue in JasonComponentFactory in method build:withJSON:withOptions::

NSClassFromString(componentClassName) returns nil for Swift classes, because Swift class name have a different format: <appName>.<className> or <appName>_<targetName>.<className> instead of just <className> in Obj-C classes.

Therefore component class names must have a prefix for Swift classes, i.e. MyAppName.JasonSuperComponent

clsource commented 5 years ago

I believe this maybe related to this https://medium.com/@maximbilan/ios-objective-c-project-nsclassfromstring-method-for-swift-classes-dbadb721723

If you use Swift classes in the Objective C project, you may be faced with the problem, that NSClassFromString method always returns nil. You need to call NSClassFromString from Swift code first of all, and don’t forget about format:

#appName.#className

But if you have many targets in your project or the name of the project is different than the name of the target you need to use the following format:

#appName_#targetName.#className

extension NSObject {
  class func swiftClassFromString(className: String) -> AnyClass! {
    if var appName: String? = NSBundle.mainBundle().objectForInfoDictionaryKey(“CFBundleName”) as! String? {
      let fAppName = appName!.stringByReplacingOccurrencesOfString(“ “, withString: “_”, options: NSStringCompareOptions.LiteralSearch, range: nil)
      return NSClassFromString(“\(fAppName).\(className)”)
    }
    return nil;
  }
}

I havent got the opportunity to integrate swift with objective-c since swift became abi stable not so long ago.

Please provide some example code (upload a zip here) to test this.

Thanks.

RomanSlyepko commented 5 years ago

@clsource well, yes. It's related to the way Swift class names are generated and consequently to the usage of NSClassFromString in Jasonette internals.

That's why in JasonComponentFactory it should extract a raw name (without module and target names) and only then use it in [NSString stringWithFormat:@"Jason%@Component", capitalizedType]

clsource commented 5 years ago

Try this in your swift code

https://stackoverflow.com/a/30895110

All swift classes use the Product Module Name a dot and the classname for their namespace (Module.Class). If you wanted to use "MySwiftClass" name for the class within your Objective-C code; you can add @objc(MySwiftClass) annotation to expose the same swift class name (without the module):

@objc(MySwiftClass)
class MySwiftClass{
    ...
}

Then

Class myClass = NSClassFromString(@"MySwiftClass");

Will contain the class instead of being nil.

clsource commented 5 years ago

If thats not working maybe this addition to the JasonComponentFactory.m will do.


+ (UIView *)build:(UIView *)component withJSON:(NSDictionary *)child withOptions:(NSMutableDictionary *)options{

    NSString *capitalizedType = [child[@"type"] capitalizedString];
    NSString *componentClassName = [NSString stringWithFormat:@"Jason%@Component", capitalizedType];

    if(componentClassName){
        Class<JasonComponentProtocol> ComponentClass = NSClassFromString(componentClassName);

        if(!ComponentClass) {
            // Maybe a is a Swift Component. NSClassFromString return nil on these.
            // see https://stackoverflow.com/questions/28706602/nsclassfromstring-using-a-swift-file
            NSString *prefix = [[NSBundle mainBundle] infoDictionary][@"CFBundleExecutable"];
            NSString *swiftClassName = [NSString stringWithFormat:@"%@.%@", prefix, componentClassName];
            ComponentClass = NSClassFromString(swiftClassName);
        }
clsource commented 5 years ago

@RomanSlyepko it works?

RomanSlyepko commented 5 years ago

yes, @objc(MySwiftClass) works fine