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]