NativeScript 5.1:直接集成 Objective-C 代码

得益于 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;
}

这里的 viewparent 参数是我们从 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 中访问它们,就大费周章地去制作一个插件了。当然,如果你想抽离出一些逻辑并使其可复用,制作插件 仍然是最佳选择,所以请根据实际情况明智地做出选择。

blog.nativescript.org/adding-obje...

相关推荐
UXbot1 小时前
AI一次生成iOS和Android双端原型功能详解
android·前端·ios·kotlin·交互·swift
我是卡卡啊1 小时前
View 绘制深度分析:HWUI · RenderThread · SurfaceFlinger
前端
产品经理爱开发1 小时前
国内免费快速HTML托管平台推荐:优先艾可秀,零门槛秒上线
前端·html
蜡台1 小时前
idea 配置 vue 运行命令时, scripts 一栏始终为空
前端·vue.js·intellij-idea
杨前端布洛芬1 小时前
仿某钉打卡 UniApp 版
前端
超绝大帅哥1 小时前
RAG检索策略及划分策略
前端
小盼江1 小时前
Uniapp小程序鲜花商城推荐系统 买家卖家双端(web+uniapp)
前端·小程序·uni-app
lihaozecq1 小时前
Agent 工具系统搭建:4 个内置工具让 Agent 学会写代码
前端
px不是xp1 小时前
【灶台导航】 RAG系统的容错设计:从向量搜索到关键词降级,一个都不能少
javascript·微信小程序·notepad++·rag