阅读视图

发现新文章,点击刷新页面。

NativeScript 的 SwiftUI 入门指南

本文中,我们将演示如何在 NativeScript 中使用 SwiftUI,共同探索构建精彩用户界面的新可能性。

前提条件

  • macOS Catalina 或更高版本
  • Xcode 11 或更高版本
  • 运行 iOS 13 或更高版本的 iOS 设备/模拟器

SwiftUI 概念

现代 iOS 开发主要使用 Swift 编程语言。SwiftUI 采用了声明式的语法——你只需说明用户界面应当做什么。

我建议你先浏览一下官方的 SwiftUI 教程,以便熟悉基本概念。

创建 NativeScript 应用

我们可以使用标准的 TypeScript 模板来创建一个应用:

ns create swiftui --ts
cd swiftui

这会搭建一个通常被称为“原生风味”(vanilla)的 NativeScript 应用。换句话说,它提供了基本的数据绑定能力以及相当简单的设置。不过,我们在此介绍的内容适用于任何框架(Angular、React、Svelte、Vue 等)。

SwiftUI 插件

安装 SwiftUI 插件

npm install @nativescript/swift-ui

注意: 你的最低 iOS 部署目标应至少为 13。

你可以在 App_Resources/iOS/build.xcconfig 文件中添加这一行:

IPHONEOS_DEPLOYMENT_TARGET = 13.0;

使用 SwiftUI

A. 创建你的 SwiftUI 视图

创建 App_Resources/iOS/src/SampleView.swift

import SwiftUI

struct SampleView: View {

  var body: some View {
    VStack {
      Text("Hello World")
        .padding()
    }
  }
}

B. 创建你的 SwiftUI 提供者 (Provider)

这将为你的 SwiftUI 准备好与 NativeScript 的双向数据绑定。

创建 App_Resources/iOS/src/SampleViewProvider.swift

import SwiftUI

@objc
class SampleViewProvider: UIViewController, SwiftUIProvider {

  // MARK: 初始化

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  required public init() {
    super.init(nibName: nil, bundle: nil)
  }

  public override func viewDidLoad() {
    super.viewDidLoad()
    setupSwiftUIView(content: swiftUIView)
  }

  // MARK: 私有部分

  private var swiftUIView = SampleView()

  /// 从 NativeScript 接收数据
  func updateData(data: NSDictionary) {
      // 可以留空
  }

  /// 允许向 NativeScript 发送数据
  var onEvent: ((NSDictionary) -> ())?
}

C. 插入到任意 NativeScript 布局中

  • app/main-page.xml
<Page
  xmlns="http://schemas.nativescript.org/tns.xsd" 
  xmlns:sw="@nativescript/swift-ui" 
  class="page"
>
  <StackLayout>
    <sw:SwiftUI swiftId="sampleView" height="100" />
  </StackLayout>
</Page>

D. 通过 swiftId 注册你的 SwiftUI 视图

这一步可以在 NativeScript 应用的启动引导文件中完成(通常是 app.tsmain.ts)。

  • app.ts
import { 
  registerSwiftUI, 
  UIDataDriver
} from "@nativescript/swift-ui";

// A. 你可以使用 'ns typings ios' 为你自己的 Swift Provider 生成类型定义
// B. 否则,可以通过声明你知道已提供的类名来忽略此步骤
declare const SampleViewProvider: any;

registerSwiftUI("sampleView", (view) =>
  new UIDataDriver(SampleViewProvider.alloc().init(), view)
);

现在你可以使用 ns debug ios 来运行应用了。

使用 Xcode 开发你的 SwiftUI

运行项目一次后,你就可以在 Xcode 中打开它,利用 Xcode 强大的智能感知辅助,进一步开发你的 SwiftUI。例如,在项目的根目录下执行:

open platforms/ios/swiftui.xcworkspace

你会发现你的 .swift 代码位于 TNSNativeSource 文件夹下,如下图所示

在这里插入图片描述

基础视图应用截图

在这里插入图片描述

高级 SwiftUI 集成

让我们深入一点,建立 SwiftUI 和 NativeScript 之间的数据绑定和事件交互。

创建 SwiftUI 组件

这可以是你想在 NativeScript 中使用的任何 SwiftUI 视图。

创建 App_Resources/iOS/src/SampleView.swift

import SwiftUI

class ButtonProps: ObservableObject {
  @Published var count: Int = 0
  var incrementCount: (() -> Void)?
}

struct SampleView: View {

  @ObservedObject var props = ButtonProps()

  var body: some View {
      VStack(alignment: .center, spacing: 0) {
          HStack(alignment:.center) {
              Text("Count $props.count)")
                  .padding()
                  .scaledToFill()
                  .minimumScaleFactor(0.5)
          }
          HStack(alignment: .center) {
              Button(action: {
                  self.props.incrementCount?()
              }) {
                  Image(systemName: "plus.circle.fill")
                      .foregroundColor(.white)
                      .padding()
                      .background(LinearGradient(
                          gradient: Gradient(
                              colors: [Color.purple, Color.pink]), startPoint: .top, endPoint: .bottom
                      ))
                      .clipShape(Circle())
              }
          }
      }
      .padding()
      .clipShape(Circle())
  }
}

创建 App_Resources/iOS/src/SampleViewProvider.swift

import SwiftUI

@objc
class SampleViewProvider: UIViewController, SwiftUIProvider {

  // MARK: 初始化

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  required public init() {
    super.init(nibName: nil, bundle: nil)
  }

  public override func viewDidLoad() {
    super.viewDidLoad()
    setupSwiftUIView(content: swiftUIView)
    registerObservers()
  }

  // MARK: 私有部分

  private var swiftUIView = SampleView()

  private func registerObservers() {
    swiftUIView.props.incrementCount = {
      let count = self.swiftUIView.props.count + 1
      // 更新 swiftUI 视图
      self.swiftUIView.props.count = count
      // 通知 nativescript
      self.onEvent?(["count": count])
    }
  }

  // MARK: API

  /// 从 NativeScript 接收数据
  func updateData(data: NSDictionary) {
    if let count = data.value(forKey: "count") as? Int {
      // 更新 swiftUI 视图
      swiftUIView.props.count = count
      // 通知 nativescript
      self.onEvent?(["count": count])
    }
  }

  /// 向 NativeScript 发送数据
  var onEvent: ((NSDictionary) -> Void)?
}

在 NativeScript 布局中使用你的 SwiftUI

  • app/main-page.xml:
<Page
  xmlns="http://schemas.nativescript.org/tns.xsd"
  xmlns:sw="@nativescript/swift-ui"
  navigatingTo="navigatingTo"
>
  <StackLayout>
    <sw:SwiftUI swiftId="sampleView" data="{{ nativeCount }}" swiftUIEvent="{{ onEvent }}" loaded="{{ loadedSwiftUI }}" />
    <Label text="{{ 'NativeScript Label: ' + nativeCount.count }}" class="h2" />
    <Button text="NativeScript 数据绑定: 减少" tap="{{ updateNativeScriptData }}" class="btn btn-primary" />
    <Button text="SwiftUI 数据绑定: 减少" tap="{{ updateSwiftData }}" class="btn btn-primary" />
  </StackLayout>
</Page>
  • app/main-page.ts:
import {
  registerSwiftUI,
  UIDataDriver,
  SwiftUI,
  SwiftUIEventData,
} from "@nativescript/swift-ui";
import { 
  EventData,
  Observable,
  Page
} from "@nativescript/core";

// A. 你可以使用 'ns typings ios' 为你自己的 Swift Provider 生成类型定义
// B. 否则,可以通过声明你知道已提供的类名来忽略此步骤
declare const SampleViewProvider: any;

registerSwiftUI("sampleView", (view) =>
  new UIDataDriver(SampleViewProvider.alloc().init(), view)
);

interface CountData {
  count: number;
}

export function navigatingTo(args: EventData) {
  const page = <Page>args.object;
  page.bindingContext = new DemoModel();
}

export class DemoModel extends Observable {
  swiftui: SwiftUI;
  nativeCount = {
    count: 0,
  };

  loadedSwiftUI(args) {
    this.swiftui = args.object;
  }

  onEvent(evt: SwiftUIEventData<CountData>) {
    this.set("nativeCount", { count: evt.data.count });
  }

  updateNativeScriptData() {
    this.set('nativeCount', { count: this.nativeCount.count - 1 });
  }

  updateSwiftData() {
    this.swiftui.updateData({ count: this.nativeCount.count - 1 });
  }
}

dev.to/valorsoftwa…

NativeScript iOS 平台开发技巧

升级到 NativeScript 8.7 后出现 APPLE is not defined 错误

出现了__APPLE__ is not defined 错误,是在你将 @nativescript/core, @nativescript/ios, @nativescript/android 升级到 ^8.7.0 版本后可能遇到的一个烦人错误。

官方推荐所有人都升级到 NativeScript 8.7,因为它包含了许多错误修复和改进,例如 devtool 以及恢复了从 8.4 版本开始中断的网络检查功能。然而有些人可能会遇到像下面这样的奇怪错误:

System.err: ReferenceError: __APPLE__ is not defined
System.err:
System.err: StackTrace:
System.err: ./node_modules/@nativescript/core/accessibility/font-scale-common.js(file: src/webpack:/FarmOps/node_modules/@nativescript/core/accessibility/font-scale-common.js:1:7)

原因

__APPLE__ is not defined 错误是由于 NativeScript 在他们的构建代码中引入了一些新的占位符。这些占位符依赖 Webpack 在构建时进行替换。而这个逻辑是在 @nativescript/webpack 5.0.19 中引入的。所以关键是确保你使用的 @nativescript/webpack 至少是 5.0.19 版本,才能成功使用 NativeScript 8.7 构建你的项目。

解决方案

所以基本上,解决 __APPLE__ is not defined 错误的方法是确保两件事:

  1. 首先,确保 @nativescript/webpack 的版本在你的 package.json 中没有被限制,像这样是最好的:^5.0.0
  2. 其次,确保你的 npm 已经知晓了 @nativescript/webpack 的最新可用版本,并且没有任何缓存。对我而言,我会执行 rm -rf node_modulesrm package-lock.json 然后再重新运行 npm i 来确保。或者更简单地,执行 ns clean 然后重新运行。

你总是可以尝试查看 package-lock.json找到 @nativescript/webpack 部分。如果它看起来像这样: 在这里插入图片描述

这表明实际安装的版本是 5.0.18,这是不行的。需要用我上面提到的任一种方法来解决。

在确保 @nativescript/webpack 版本没问题后,你现在可以再次运行 ns run 来继续你的 NativeScript 开发工作。

附言:如果你正在经历常见的 NativeScript 问题,并且需要一些快速修复或解决方法,请务必查看我们的“快速修复”部分。在这一部分,你会发现我在 NativeScript 之旅中收集的技巧和窍门,以及解决常见问题的解决方法。希望能帮助到许多像我一样的人。

NativeScript iOS: 无法启动模拟器

作为一名 iOS 开发者,最令人沮丧的事情莫过于 iOS 模拟器突然停止工作。这个工具对于在受控环境中测试和调试你的应用程序至关重要。当它失效时,你的工作流就会戛然而止,打乱工作效率并造成不必要的压力。

问题:无法启动模拟器

模拟器就是不工作,拒绝启动。并且一直说“无法启动模拟器”。 在这里插入图片描述

解决方案:

这个修复方法非常简单。

对于 Mac Ventura 13.0 及更高版本的操作系统 -> 点击 Mac 左上角的苹果图标 > 系统设置 > 搜索存储空间 > 等待加载,然后点击开发者 (Developer)。

在这里插入图片描述

在下一个屏幕中,选择删除 Xcode 缓存 (deleting Xcode Caches)。

删除完成后。尝试重新启动你的模拟器,现在它应该又能正常工作了。

如何正确修复:Info.plist 键 'BGTaskSchedulerPermittedIdentifiers' 必须包含一个标识符列表在这里插入图片描述

对于一个 NativeScript 应用,这个错误通常出现在 iOS 应用开发的上下文中,具体来说,当你或你安装的某些插件试图使用后台任务时,就会出现这个问题。

解决方法

  1. 打开你的应用的 App_Resources/iOS/Info.plist 文件。
  2. 如果尚不存在,添加键 BGTaskSchedulerPermittedIdentifiers
  3. 将其类型设置为 Array(数组)。
  4. 对于每个后台任务,在此数组中添加一项。每一项都应该是一个字符串,代表一个后台任务的唯一标识符。

使用示例

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>

如果你有任何自定义的后台任务,你也需要将其 ID 列入上面的数组中。除此之外,你可以直接使用上面这段代码。

$(PRODUCT_BUNDLE_IDENTIFIER) 将被解析为你在 nativescript.config.ts 中定义的应用 Bundle ID,例如:com.newbiescripter.myawesomeapp

请记住,在 iOS 中使用后台任务有一些限制和准则,因为苹果旨在优化电池续航和性能。请确保你使用后台任务的方式符合这些准则。

❌