得益于 NativeScript 框架最近的一些更新,你现在可以直接把 Objective-C 代码文件放到你的应用里了(不用先打包成插件!)。想了解怎么自己动手设置?接着往下看这篇博客。
在 NativeScript 5.1 版本之前,如果你想在项目里使用自定义的 Objective-C 或 Swift 代码,就得先创建一个框架,再把它打包成插件。这可能要花不少额外功夫(特别是只想加个很小的功能的时候)。所以我们想办法让开发者可以直接把 Objective-C 文件作为应用资源添加进来,省点事。
具体怎么做?
想让你的 Objective-C 文件能被 NativeScript CLI 创建的 Xcode 项目识别并使用,需要完成下面三步:
第一步:在 App_Resources/iOS 目录下创建 src 文件夹
CLI 工具会自动寻找这个文件夹,并将里面的文件内容添加到 Xcode 项目中,进行编译和链接。
第二步:把源代码文件放进 src 文件夹
第三步:创建一个模块映射文件(module map)
module.modulemap 这个文件必不可少,有了它,元数据生成器才能找到你声明的类和方法,并把它们加入到生成的抽象语法树(AST)里,这样你的 JavaScript 代码就能调用了。如果你对模块映射这个概念不熟,可以参考下这里。
提示: 当然,你可以把代码放在不同的子目录里,它们也会以同样的结构被添加到
.xcodeproj的文件树中。
示例演示
这次演示我选了一个 Daniel Larsson 写的关于 UIViewPropertyAnimator 的很棒教程。我之所以选用原生代码教程,就是想做个示范,告诉你如何查找原生实现,然后在你的 NativeScript 应用里加以利用。
如果我们要用 JavaScript 来实现类似功能,可能得把大段代码包在 if (platform.ios) 这样的条件块里或者专门创建一个插件。而我马上要展示的这种新方式,让你可以在对应平台(原生)代码里添加特定平台的功能。而且在某些情况下,这种方式性能也更好------比如动画视图时,如果每帧都需要执行一次原生调用,新方式可以避免每秒钟(理想状态下)通过桥接调用60次。
顺便说一句,NativeScript 在这种场景下表现也相当不错:

我们来创建 NativeAnimator 类。写 Objective-C 代码需要两个文件。
别忘了,这两个源文件必须放在 App_Resources/iOS/src 目录下。通常情况下,你需要自己手动创建这个 src 文件夹。
NativeAnimator.h
objc
#import <Foundation/Foundation.h>
@interface NativeAnimator : NSObject
-(id)initWithView:(UIView*)view andParent:(UIView*)parent;
-(void)setup;
@end
我们希望把实现细节尽可能隐藏起来,所以 NativeAnimator 只暴露了这两个方法。我喜欢单独用一个 setup 方法来添加 panGestureRecognizer,这样能让初始化方法职责更单一。
objc
-(id)initWithView:(UIView*)view andParent:(UIView*)parent {
self = [super init];
if (self) {
self.playerView = view;
self.parentView = parent;
}
return self;
}
这里的 view 和 parent 参数是我们从 JavaScript 代码里传过来的视图。NativeAnimator 需要持有它们的引用才能完成工作。现在我们可以创建 panGestureRecognizer 了:
objc
-(void)setup {
self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self.parentView addGestureRecognizer:self.panGestureRecognizer];
}
最终的完整实现是这样的:
NativeAnimator.m
objc
#import "NativeAnimator.h"
typedef NS_ENUM(NSInteger, PlayerState) {
PlayerStateThumbnail,
PlayerStateFullscreen,
};
@interface NativeAnimator ()
@property (weak, nonatomic) UIView *parentView;
@property (weak, nonatomic) UIView *playerView;
@property (nonatomic) UIViewPropertyAnimator *playerViewAnimator;
@property (nonatomic) CGRect originalPlayerViewFrame;
@property (nonatomic) PlayerState playerState;
@property (nonatomic) UIPanGestureRecognizer *panGestureRecognizer;
@property (nonatomic) UIView* b;
@end
@implementation NativeAnimator
-(id)initWithView:(UIView*)view andParent:(UIView*)parent {
self = [super init];
if (self) {
self.playerView = view;
self.parentView = parent;
}
return self;
}
-(void)setup {
self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self.parentView addGestureRecognizer:self.panGestureRecognizer];
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:self.parentView.superview];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
[self panningBegan];
}
if (recognizer.state == UIGestureRecognizerStateEnded)
{
CGPoint velocity = [recognizer velocityInView:self.parentView];
[self panningEndedWithTranslation:translation velocity:velocity];
}
else
{
CGPoint translation = [recognizer translationInView:self.parentView.superview];
[self panningChangedWithTranslation:translation];
}
}
- (void)panningBegan
{
if (self.playerViewAnimator.isRunning)
{
return;
}
CGRect targetFrame;
switch (self.playerState) {
case PlayerStateThumbnail:
self.originalPlayerViewFrame = self.playerView.frame;
targetFrame = self.parentView.frame;
break;
case PlayerStateFullscreen:
targetFrame = self.originalPlayerViewFrame;
}
self.playerViewAnimator = [[UIViewPropertyAnimator alloc] initWithDuration:0.5 dampingRatio:0.8 animations:^{
self.playerView.frame = targetFrame;
}];
}
- (void)panningChangedWithTranslation:(CGPoint)translation
{
if (self.playerViewAnimator.isRunning)
{
return;
}
CGFloat translatedY = self.parentView.center.y + translation.y;
CGFloat progress;
switch (self.playerState) {
case PlayerStateThumbnail:
progress = 1 - (translatedY / self.parentView.center.y);
break;
case PlayerStateFullscreen:
progress = (translatedY / self.parentView.center.y) - 1;
}
progress = MAX(0.001, MIN(0.999, progress));
self.playerViewAnimator.fractionComplete = progress;
}
- (void)panningEndedWithTranslation:(CGPoint)translation velocity:(CGPoint)velocity
{
self.panGestureRecognizer.enabled = NO;
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
__weak NativeAnimator *weakSelf = self;
switch (self.playerState) {
case PlayerStateThumbnail:
if (translation.y <= -screenHeight / 3 || velocity.y <= -100)
{
self.playerViewAnimator.reversed = NO;
[self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
weakSelf.playerState = PlayerStateFullscreen;
weakSelf.panGestureRecognizer.enabled = YES;
}];
}
else
{
self.playerViewAnimator.reversed = YES;
[self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
weakSelf.playerState = PlayerStateThumbnail;
weakSelf.panGestureRecognizer.enabled = YES;
}];
}
break;
case PlayerStateFullscreen:
if (translation.y >= screenHeight / 3 || velocity.y >= 100)
{
self.playerViewAnimator.reversed = NO;
[self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
weakSelf.playerState = PlayerStateThumbnail;
weakSelf.panGestureRecognizer.enabled = YES;
}];
}
else
{
self.playerViewAnimator.reversed = YES;
[self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
weakSelf.playerState = PlayerStateFullscreen;
weakSelf.panGestureRecognizer.enabled = YES;
}];
}
}
CGVector velocityVector = CGVectorMake(velocity.x / 100, velocity.y / 100);
UISpringTimingParameters *springParameters = [[UISpringTimingParameters alloc] initWithDampingRatio:0.8 initialVelocity:velocityVector];
[self.playerViewAnimator continueAnimationWithTimingParameters:springParameters durationFactor:1.0];
}
@end
还差一个文件,就是 modulemap。长这样:
module.modulemap
arduino
module NativeAnimator {
header "NativeAnimator.h"
export *
}
如果你运行 tns prepare ios 命令,就会发现 NativeAnimator 的文件已经成功加入项目了。干得漂亮!想看到我们刚创建的 NativeAnimator 实际跑起来的效果,还需要创建一个它能操作的视图。
我假设你已经创建好了一个基础的 JavaScript Hello World 应用!
一个不错的入手点是在 onNavigatingTo 函数里,因为我们可以在这里拿到原生的 UIViewController。
js
function onNavigatingTo(args) {
const page = args.object;
page.ios.playerView = UIView.alloc().initWithFrame(CGRectMake(100, 500, 100, 100));
page.ios.playerView.backgroundColor = UIColor.blackColor;
page.ios.view.addSubview(page.ios.playerView);
page.ios.animator = NativeAnimator.alloc().initWithViewAndParent(page.ios.playerView, page.ios.view);
page.ios.animator.setup();
...
}
效果如下:

总结
现在可以直接往 NativeScript 应用里添加 Objective-C 源码,再也不用为了创建几个 Objective-C 类并从 JavaScript 中访问它们,就大费周章地去制作一个插件了。当然,如果你想抽离出一些逻辑并使其可复用,制作插件 仍然是最佳选择,所以请根据实际情况明智地做出选择。