Search code examples
iosxcodecordovacordova-pluginsplugin.xml

Cordova Plugin: Add custom framework using weak linking


In my plugin.xml, I'm trying to declare a custom .framework and have it weak linked, but once I open Xcode I see the added framework is still marked as "required" instead of "optional".

Here's my plugin.xml entry:

<framework src="ios/libs/BlaBla.framework" custom="true" weak="true" />

It's a 3rd party custom .framework I've received that contains Headers (obviously) and a shared dynamic lib file (which I will load during runtime using dlopen("TheDylib", RTLD_LAZY|RTLD_GLOBAL);).

The reason I can't use <header-file src="BlaBla.framework/Headers/Bla.h" /> is that the headers in the .framework themselves refer to inner headers with #import <BlaBla.framework/SomeHeader.h> so the <header-file> tag can't help in this case.


Solution

  • Important note

    Better use "Embedded Frameworks" functionality instead of this solution because dlopen is forbidden since iOS 8.0 on non-mac/simulator devices (real iPhones/iPads).

    Take a look at Custom Cordova Plugin: Add framework to "Embedded Binaries"

    END OF Important note

    I ended up doing something a bit different, Instead of declaring the .framework as a <framework ... /> tag, I did the following.

    I created a plugin hook that adds the plugin dir to the FRAMEWORK_SEARCH_PATHS Xcode build property.

    <hook type="after_platform_add" src="hooks/addPluginDirToFrameworkSearchPaths/hook.js" />
    

    Hook Code:

    module.exports = function(context) {
        const includesiOS = context.opts.platforms.indexOf('ios') != -1;
        if(!includesiOS) return;
    
        const
            deferral = context.requireCordovaModule('q').defer(),
            pluginId =  context.opts.plugin.id;
    
        const xcode = require('xcode'),
            fs = require('fs'),
            path = require('path');
    
        function fromDir(startPath,filter, rec) {
            if (!fs.existsSync(startPath)){
                console.log("no dir ", startPath);
                return;
            }
    
            const files=fs.readdirSync(startPath);
            for(var i=0;i<files.length;i++){
                var filename=path.join(startPath,files[i]);
                var stat = fs.lstatSync(filename);
                if (stat.isDirectory() && rec){
                    fromDir(filename,filter); //recurse
                }
    
                if (filename.indexOf(filter)>=0) {
                    return filename;
                }
            }
        }
    
        const xcodeProjPath = fromDir('platforms/ios','.xcodeproj', false);
        const projectPath = xcodeProjPath + '/project.pbxproj';
        const myProj = xcode.project(projectPath);
    
        function unquote(str) {
            if (str) return str.replace(/^"(.*)"$/, "$1");
        }
    
        function getProjectName(myProj) {
            var projectName = myProj.getFirstTarget().firstTarget.name;
            projectName = unquote(projectName);
            return projectName;
        }
    
        function set_FRAMEWORK_SEARCH_PATHS(proj) {
            const lineToAdd = '"\\"' + getProjectName(proj) + '/Plugins/' + pluginId + '\\""'
    
            const FRAMEWORK_SEARCH_PATHS =  proj.getBuildProperty("FRAMEWORK_SEARCH_PATHS");
            if(FRAMEWORK_SEARCH_PATHS != null) {
                const isArray = typeof FRAMEWORK_SEARCH_PATHS != 'string';
                if(isArray) {
                    for(var entry of FRAMEWORK_SEARCH_PATHS) {
                        if(entry.indexOf(pluginId) != -1) {
                            return false; // already exists, no need to do anything.
                        }
                    }
                } else { // string
                    if(FRAMEWORK_SEARCH_PATHS.indexOf(pluginId) != -1) {
                        return false; // already exists, no need to do anything.
                    }
                }
    
                var newValueArray = isArray?FRAMEWORK_SEARCH_PATHS:[FRAMEWORK_SEARCH_PATHS];
                newValueArray.push(lineToAdd);
    
                proj.updateBuildProperty("FRAMEWORK_SEARCH_PATHS", newValueArray);
            } else {
                proj.addBuildProperty("FRAMEWORK_SEARCH_PATHS", lineToAdd);
            }
            return true;
        }
    
        myProj.parse(function (err) {
            if(err) {
                deferral.reject('Error while parsing project');
            }
    
            if(set_FRAMEWORK_SEARCH_PATHS(myProj)) {
                fs.writeFileSync(projectPath, myProj.writeSync());
                console.log('Added Framework Search Path for ' + pluginId);
            } else {
                console.log('Framework Search Path was already added for ' + pluginId);
            }
    
            deferral.resolve();
        });
    
        return deferral.promise;
    };
    

    Note: the hook depends on an NPM dependency named "xcode" so do npm i xcode --save before (no need to edit the hook code). Now the way we declare in plugin.xml to import the .framework content to our project is the following:

    <source-file src="ios/libs/CameraWizard.framework" />
    <resource-file src="ios/libs/CameraWizard.framework/CameraWizard" />
    

    We use the source-file tag simply to import the .framework because we only want it to be copied to the iOS platform plugins directory, we do not wish to have it "strongly" linked, the reason we need it there is only for it's Headers, not it's binary. Our hook will add the correct framework search path for it.

    Then we use resource-file to import only the shared library file inside the .framework directory, we add it as a resource so that when the app starts and we call dlopen(...), the shared library will be found in runtime.

    Finally, Now to use the shared library in your plugin code, do the following:

    1. #import <dlfcn.h> (also import your .framework's headers).
    2. Under -(void)pluginInitialize method, load the shared lib:

      NSString* resourcePath = [[NSBundle mainBundle] resourcePath]; NSString* dlPath = [NSString stringWithFormat: @"%@/FrameworkFileNameInResourceTag", resourcePath]; const char* cdlpath = [dlPath UTF8String]; dlopen(cdlpath, RTLD_LAZY|RTLD_GLOBAL);

    3. Now to use a class from the shared library:

      SomeClassInFramework someInstance = [(SomeClassInFramework)[NSClassFromString(@"SomeClassInFramework") alloc] init];