返回博客首页
← 所有文章

将 Objective-C 代码添加到 NativeScript 应用

2019 年 4 月 2 日 — 作者:Teodor Dermendzhiev

在 NativeScript 5.1 之前,您需要创建一个框架并将其包装在插件中才能使用自定义的 Objective-C/Swift 代码。由于这可能需要很多不必要的工作(尤其是在您想要添加一个小功能时),我们尝试使直接将 Objective-C 文件作为应用程序资源添加成为可能。

操作方法

要将您的 Objective-C 文件添加到 NativeScript CLI Xcode 项目中,您需要执行三件事

1. 在 App_Resources/iOS 中创建 src 目录

CLI 将在此文件夹中查找,以便将它的内容添加到 Xcode 项目并编译和链接源文件。

2. 在 src 文件夹中添加源文件

3. 创建一个模块映射

module.modulemap 文件是必要的,这样元数据生成器才能找到声明并将它们添加到生成的 AST 中,从而使它们可以从您的 JavaScript 代码中访问。如果您不熟悉模块映射的概念,这里是一个很好的起点

注意:当然,您可以将代码组织到单独的目录中,并且它们将作为 .xcodeproj 文件树中的单独目录添加。

演示

对于这个演示,我选择了一个由 Daniel Larsson 撰写的关于 UIViewPropertyAnimator 的很棒教程。我使用原生代码教程的原因之一是为了作为示例,说明如何查找原生实现,然后在您的 NativeScript 应用中使用它们。

现在,如果我们要在 JavaScript 中实现它,我们可能需要将大块代码包装在 if (platform.ios) 块中,或者创建一个插件。我将要展示的另一种方法是在平台特定的(原生)代码中添加平台特定的功能。此外,在某些情况下,它在性能方面也可能更好 - 例如,当原生调用应该在动画视图时每帧执行一次,这样桥接器就不会每秒调用 60 次(在最好的情况下)。

顺便说一句,NativeScript 在这种情况下表现得非常好。

example of dragging with javascript

让我们从创建 NativeAnimator 类开始。因为我们正在编写 Objective-C 代码,所以现在我们需要两个文件。

不要忘记源文件需要位于 App_Resources/iOS/src 中。大多数情况下,您需要自己创建 src 目录。

NativeAnimator.h

#import <Foundation/Foundation.h>

@interface NativeAnimator : NSObject

-(id)initWithView:(UIView*)view andParent:(UIView*)parent;
-(void)setup;

@end

我们希望尽可能地隐藏实现细节,这就是为什么这些是 NativeAnimator 公开的唯一方法的原因。我更喜欢为附加 panGestureRecognizer 创建一个单独的方法,以便保持初始化程序只负责它应该负责的内容。

-(id)initWithView:(UIView*)view andParent:(UIView*)parent {
    self = [super init];
    if (self) {
        self.playerView = view;
        self.parentView = parent;
    }

    return self;
}

viewparent 参数是我们从 JavaScript 代码中传递的视图。NativeAnimator 需要保留对它们的引用才能完成其工作。现在我们准备创建 panGestureRecognizer

-(void)setup {
    self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [self.parentView addGestureRecognizer:self.panGestureRecognizer];
}

以下是最终的实现代码

NativeAnimator.m

#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

module NativeAnimator {
    header "NativeAnimator.h"
    export *
}

如果您运行 tns prepare ios,您将看到 NativeAnimator 文件现在是项目的一部分。干得好!

为了看到我们新创建的 NativeAnimator 实际工作,我们需要创建它将要使用的视图。

我假设您已经创建了一个普通的 js Hello World 应用!

一个很好的地方是在 onNavigatingTo 函数中执行此操作,因为我们可以在那里获取原生的 UIViewController

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();
    ...
}

这是结果

example of dragging with native code

总结

能够直接将 Objective-C 源代码添加到您的 NativeScript 应用中,消除了每次您需要创建 Objective-C 类并从 JavaScript 中访问它们时都要创建插件的麻烦。当然,创建插件 仍然是您想要抽象一些逻辑并使其可重用的最佳选择,因此请明智地选择。