Using Native Libraries in iOS
NativeScript for iOS lets you include native libraries and consume their APIs from JavaScript.
For iOS, three types of library packages are available:
- Shared framework (
MyFramework.framework
): An ordinary shared library wrapped in a framework. Typically, contains the requiredmodule.modulemap
file. - Static framework (
MyFramework.framework
): An ordinary static library wrapped in a framework. Typically, doesn't contain the requiredmodule.modulemap
file and you need to add it manually. - Static library (
libMyLib.a
): Contains a headers folder (usually calledinclude
) with.h
files.
You can use any of the following approaches to add and use a native library in your project:
- (Recommended) Create a plugin containing a CocoaPod
Podfile
. - Create a plugin containing the already built binary and headers.
- (Not recommended) Don't create a plugin and manually change the Xcode project located in
{your-app}/platforms/ios/
.
To consume a native library the iOS Runtime has to know about the following resources:
- Binary file (e.g
libMyLib.a
,MyLib
). - Header files and
module.modulemap
file describing a clang module and specifying which headers are part of the module.
The only reason the runtime needs header files is to generate metadata. The metadata generator knows which headers have to be parsed because of the supplied module.modulemap
file. Both the headers and module.modulemap
file must reside in a folder which is part of the header search paths of the Xcode project ({your-app}/platforms/ios/{your-app}.xcodeproj
). You can find a sample module.modulemap
file here. You can find more information about CLANG modules, module maps and their synthax here: https://clang.llvm.org/docs/Modules.html
Shared Frameworks
Shared frameworks are the best option because only they have a well-known structure and a module.modulemap
file which eliminates the need for manual work. NativeScript plugins support shared frameworks and you can add them with CocoaPods.
With CocoaPods, you can remove the framework (with all the binary and header files in it) from your plugin repository and keep only a single Podfile
. You also get all the benefits of using a package manager.
If there is no CocoaPod for the current library you can still use a plugin, but the framework must be dropped in the plugin folder ({your-plugin}/platforms/ios/{MyFramework}.framework
) and you lose all the benefits of using a package manager.
Pros
- Can be included by NativeScript plugin.
- Can be included in the plugin by a
Podfile
(if apod
for the library exists). - There is no need to manually edit the library before adding it.
There is no need to manually edit the app after adding the library.
Cons
Shared frameworks can be used only in iOS 8 and above. This limitation is valid for pure native applications, too. If you are targeting iOS versions lower than 8.0 you must use static frameworks.
Static Frameworks
Most of the static frameworks don't contain module.modulemap
file, so you have to add the file manually. To include a static framework in a plugin grab a prebuilt version of the framework, add a module.modulemap
file in it and drop it in your {plugin-path}/platforms/ios/
folder.
In case you cannot modify the native framework (for example when it comes from a Pod) and must define its
module.modulemap
somewhere else in your plugin, take a look at the following sample for guidance: https://github.com/NativeScript/plugin-ios-modulemap-sample
Pros
- Can be included by NativeScript plugin.
- There is no need to manually edit the app after adding the library (but you have to manually edit the framework in order to add
module.modulemap
file).
Cons
- Manual changes of the framework are required (add
module.modulemap
file). - Only Objective-C APIs are exposed (no C functions and C constants) from static frameworks. To work around this limitation, you can manually edit the Xcode project file. However, this workaround is not recommended.
Static Libraries
The NativeScript CLI supports static libraries coming from plugins but the binary and headers must be ordered in a specific folder structure described in details here. This is required because the NativeScript CLI generates a module.modulemap
file for the library which works most of the time. However, in some cases you might need to wrap the library in a static framework with a module.modulemap
file.
If you cannot wrap your static library in a static framework with a
module.modulemap
, in cases such as when using Cocoapods, take a look at the following sample for guidance: https://github.com/NativeScript/plugin-ios-modulemap-sample
Pros
- Can be included by NativeScript plugin.
- It works without manual changes but not in all cases.
It is trivial to wrap a static library in a static framework. Just put all the headers and binary files in the proper folder structure, add a
module.modulemap
and you have a static framework which works in all cases.
Cons
- Can't be included by a
Podfile
. - In some cases, you must add a
module.modulemap
file manually. - You must wrap the library in a static framework if the automatic
module.modulemap
file generation does not succeed. - Only Objective-C APIs are exposed (no C functions and C constants) from static libraries. To work around this limitation, you can manually edit the Xcode project file. However, this workaround is not recommended.
NativeScript plugins also support merging of .plist
files. If a library requires changes in Info.plist
, the plugin can handle that without you touching the /platforms/ios/
folder. However, there are libraries which require more complex manipulations of the Xcode project file, which can't be achieved with plugins. In these cases, the only solution is to do it manually. Keep in mind that after updating the iOS platform, your manual changes might be lost.
APIs written in Swift
CocoaPod libraries written in Swift can be called from NativeScript only if they are exposed to Objective-C. This means that the following conditions have to be met:
- The methods and types must have
public
oropen
access. For more information on Access Control read this article - Classes need to inherit from
NSObject
or some other Objective-C class in order to be exposed. Refs Swift Migration Guide - Starting from Swift 4.0, types and methods have to be explicitly marked with
@objc
or@objcMembers
attributes. You can read more about them here.
NOTE: To be able to override a Swift method in its JavaScript inheritor it MUST use the message dispatch calling mechanism. This is enforced by marking the method with the
dynamic
keyword.NOTE: You can avoid adding
@objc
attribute for every member you'd like to expose by settingSWIFT_SWIFT3_OBJC_INFERENCE
toOn
. This has the drawback that it will cause deprecation warnings during build and deprecation logs at runtime. SamplePodfile
:.... post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_SWIFT3_OBJC_INFERENCE'] = 'On' end end end
Conclusion
As a rule of thumb, avoid manual changes to the Xcode project file in the /platforms/ios
folder. Always try to use CocoaPods with NativeScript plugins and shared frameworks. The second best option is a prebuilt static framework with manually added module.modulemap
file, wrapped in a NativeScript plugin. Use the other options only as a last resort after making sure there is no better solution.
Troubleshooting
Metadata in human readable format
Starting with version 1.4 of NativeScript for iOS, you are able to generate debug metadata and TypeScript declarations for third-party libraries. This way you are able to see exactly what APIs are exposed to JavaScript.
Executing the following command from the root of your NativeScript app produces a metadata
folder with a .yaml
file for each Clang module:
$ TNS_DEBUG_METADATA_PATH="$(pwd)/metadata" tns build ios [--for-device] [--release]
Generating TypeScript typings
Executing the following command from the root of your NativeScript app produces a typings
folder with a .d.ts
file for each Clang module:
$ TNS_TYPESCRIPT_DECLARATIONS_PATH="$(pwd)/typings" tns build ios [--for-device] [--release]
If you have downloaded the documentation set for iOS, the command above will also include brief description in the form of a comment above every symbol in the generated typings
(currently not supported for Xcode 8+). Most IDEs which support typescript IntelliSense will make use of these comments. Furthermore, you can generate structured documentation from these comments with tools like TypeDoc.
Metadata generator's parsing errors and warnings
The stderr
output of the metadata generator (including all errors and warnings emitted by the Objective-C parser) is redirected
to a separate log file. It is located in platforms/ios/build/<configuration>-<target>/metadata-generation-stderr-<arch>.txt
under the main project dir.
The reason behind this decision is that sometimes projects or plugins may have dependencies which are not designed to be fed to an Objective-C compiler. When attempting to generate the metadata for such projects, the metadata generator's error output would pollute Xcode's build output with lines which would look like compilation errors/warnings and would confuse both users and IDE parsers that the compiler emitted them. One example for such library is the LevelDB CocoaPod which is meant to be used in C++ context only. It is included in all projects using the NativeScript Firebase plugin because it's a dependency of the FirebaseDatabase CocoaPod. Generating metadata from this CocoaPod is expected to fail as the iOS Runtime doesn't parse and expose C++ entities to JS. So it's preferable to keep all these errors away from the actual application build output.
IMPORTANT: In cases where the metadata for some native entities is missing, this log file can turn out to be invaluable in tracking down the reasons. It should be the first place to start looking for clues about what might have gone wrong.
Sometimes the reason may be an incorrect
#include
statement. In such cases, in order to see the real error you will also have to run the metadata generator in strict includes mode
Enabling strict includes mode
Starting with version 5.4 of {N} you can set the TNS_DEBUG_METADATA_STRICT_INCLUDES
environment variable to diagnose the reasons for missing
metadata entities when no errors related to their respective source files can be found in metadata generator's stderr log.
When this setting is enabled, #include
errors will be caught and logged in the stderr output but some Pod libraries might cause significantly less metadata
being parsed and generated, so it really should be used only when debugging issues with missing metadata.
$ TNS_DEBUG_METADATA_STRICT_INCLUDES="true" tns build ios [--for-device] [--release]