iOS Helper 工具

@ajax-js/nativescript-common 提供 iOS 原生 Helper,包含 Toast、应用版本信息、App Store 跳转和界面方向设置。适用于 NativeScript 工程,与 Vue 视图层无关。

插件最低支持 iOS 15;场景方向请求要求 iOS 16 及以上。Android 和 iOS 可共用 toast(message)、copyToClipboard(str) 和 getFromClipboard(),其他 Helper 工具仅支持 iOS。

安装与调用入口

独立的 Sources/com/ajaxjs/nativescript/ios/ConfirmDialog.swift 还提供自定义原生确认框。推荐通过统一 confirm(options) 调用,配色、按钮文案、长正文滚动、正文对齐与蒙层控制见 通用工具。原生 ObjC 类为 AjaxJSConfirmDialog,NativeScript 方法名 showCompletion(optionsJson, callback);JSON 颜色须为 #AARRGGBB,callback 接收 (confirmed, error),无活动窗口返回 false,显示/参数失败返回非空 error,完成最多一次。原生在主线程从活动场景最上层控制器展示,不使用 UIAlertController 私有属性。

npm install @ajax-js/nativescript-common

已发布的 npm 包携带 platforms/ios/AjaxJSCommon.xcframework,NativeScript 在 iOS 构建时集成原生组件,无需在宿主重复复制 Swift 源码。

import { isIOS } from '@nativescript/core';
import { toast, com as commonNative } from '@ajax-js/nativescript-common';

const iosHelper = commonNative.ajaxjs.nativescript.ios.Helper;

toast('操作成功');

if (isIOS) {
    const version = iosHelper.getAppVersionName();
    const build = iosHelper.getAppBuildNumber();
    console.log(`应用版本:${version},构建版本:${build}`);
}

com 是插件导出的 TS 命名空间,请从插件显式导入。它不是 iOS 系统提供的全局 Java 包;使用 com as commonNative 可以避免与 Android 全局 com 混淆。仅创建上述 iosHelper 引用不会调用原生方法。

所有 Helper 方法均为静态方法,无需创建实例。在非 iOS 平台调用这些方法会抛出 Error;缺少 XCFramework 时也会抛出 Error。

方法概览

方法 参数 返回值 最低系统版本
copyToClipboard(text) 原始字符串,允许空字符串 void,同步写入 iOS 15
getFromClipboard() 无 字符串,不可读时为空字符串 iOS 15
showToast(message) 提示字符串 void iOS 15
getAppVersionName() 无 版本名称字符串 iOS 15
getAppBuildNumber() 无 构建版本字符串 iOS 15
openAppInAppStore(appleID, isInternational?) 数字 ID 字符串;是否不限定地区,默认 false void iOS 15
setLandscapeOrientation() 无 void iOS 15
setPortraitOrientation() 无 void iOS 15
requestOrientationChange() 无 void iOS 16
forcePortrait() 无 void iOS 16

应用版本信息

const version: string = iosHelper.getAppVersionName();
const build: string = iosHelper.getAppBuildNumber();
方法 读取字段 读取失败时的行为
getAppVersionName() 宿主应用 Info.plist 的 CFBundleShortVersionString 记录日志并返回 "Unknown"
getAppBuildNumber() 宿主应用 Info.plist 的 CFBundleVersion 记录日志并返回 "Unknown"

读取的是当前宿主 App 的版本,不是 npm 插件或 Framework 的版本。构建版本按字符串返回,不应假定为整数。

打开 App Store

/*
 * 示例 ID 请替换为实际应用的 App Store 数字 ID。
 */
const appleID = '123456789';

iosHelper.openAppInAppStore(appleID);
iosHelper.openAppInAppStore(appleID, true);

openAppInAppStore(appleID: string, isInternational = false): void:

  • appleID 必须是非空的 ASCII 数字字符串,不包含 id 前缀、空格或完整 URL。
  • 默认 false 使用中国区路径 /cn/app/id…。
  • true 使用不限定地区的路径 /app/id…,不会自动选择另一个应用 ID。国内版、国际版若有不同 ID,须由调用方传入对应值。

组件先尝试 itms-apps://apps.apple.com…,根据实际打开回调判断结果;失败后尝试对应的 https://apps.apple.com… 链接。无效 ID、URL 构造失败或两个入口均失败时记录日志。

该方法允许从后台线程调用,内部切换到主线程。返回 void,不等待打开完成,不提供打开成功的布尔值或 Promise。

界面方向

iOS 16 及以上:场景请求

import { Device, isIOS } from '@nativescript/core';

if (isIOS && Number.parseInt(Device.osVersion, 10) >= 16) {
    iosHelper.requestOrientationChange();
}
  • requestOrientationChange() 请求窗口场景切换为横屏向右。
  • forcePortrait() 请求窗口场景切换为竖屏。

TS 包装层在低于 iOS 16 时抛出 Error。原生调用异步切回主线程;当前实现从 connectedScenes 的首个场景获取 UIWindowScene,多窗口应用不能通过参数指定目标场景。没有可用场景时不执行请求,系统拒绝时通过回调记录日志。

方法名称中的“force”不代表能够绕过系统限制。宿主必须配置支持的方向,当前视图控制器也必须允许该方向;返回 void 不表示旋转一定成功。

保留的设备方向方法

iosHelper.setLandscapeOrientation();
iosHelper.setPortraitOrientation();

setLandscapeOrientation() 使用原工程的设备方向设置方式切换为横屏向左,setPortraitOrientation() 切换为竖屏。两者通过 UIDevice 的 orientation 设置方向,后台调用会异步进入主线程。

这些方法保留原实现,仍受系统和宿主方向配置限制。iOS 16 及以上建议使用场景请求方法;不要把设备方向设置视为可靠的旋转成功通知。

原生工程与桥接

Swift 源码位于:

ios/nativescipt-common/Sources/com/ajaxjs/nativescript/ios/
├── Helper.swift
└── AppToast.swift

Swift 使用 Framework 模块,没有 Java 式的包机制。上述目录组织对应 TS 的 com.ajaxjs.nativescript.ios.Helper 命名空间;原生 Helper 继承 NSObject,通过 @objcMembers 暴露方法,Objective-C 类名为 AjaxJSIOSHelper,避免与宿主已有的 Helper 冲突。AppToast 是内部实现,不提供公开调用入口。

直接通过 NativeScript 调用原生类时:

AjaxJSIOSHelper.showToast('提示消息');
const version = AjaxJSIOSHelper.getAppVersionName();
AjaxJSIOSHelper.openAppInAppStoreIsInternational('123456789', true);

业务代码优先使用 TS 包装层。直接调用原生的 requestOrientationChange()、forcePortrait() 时,必须自行检查 iOS 16 可用性。

原生日志使用 subsystem com.ajaxjs.nativescript.ios,category 为 Helper 或 Toast。版本缺失、活动窗口缺失、App Store 打开失败和方向请求失败可通过这些日志排查。

构建与验证

从源码单独重建 iOS 产物,在 nativescript-plugin/nativescipt-common 目录执行:

npm run build:ios
npm run build

构建脚本使用独立 Xcode Framework 工程,生成真机 arm64 和模拟器 arm64/x86_64 的 XCFramework,并复制到插件 platforms/ios。使用当前工程的 Xcode 27 和 iOS 15 部署目标;更新后需要重新构建宿主 App,确保加载最新原生接口。

demo-app 的“测试”tab 提供“Toast 测试”按钮,保留底部 tabs。已在 iPhone 17 Pro / iOS 26.5 模拟器验证 Toast 显示、自动消失及宿主版本读取;App Store 跳转和实际界面旋转尚未执行验证。

Token 与 MQTT 凭据安全存储

推荐使用统一的 setSecureItem/getSecureItem/removeSecureItem,详见 通用工具:安全存储。原生源码 Sources/com/ajaxjs/nativescript/ios/SecureStorage.swift,ObjC 类 AjaxJSSecureStorage,NativeScript 方法为 setItemValue(key, value)、getItem(key)、removeItem(key)。返回 CryptoResult,须先检查 error:写入/删除成功 value=true;读取不存在 value=null,已保存空字符串仍返回空字符串。

凭据使用独立 Keychain service,与 AES 密钥隔离;WhenUnlockedThisDeviceOnly、不同步,锁屏可能不可访问,卸载重装后可能仍保留。键名非空白且最多 128 UTF-16 单元,值最多 65536 UTF-8 字节;业务需要在退出登录时显式删除。

Swift HTTP 请求

Sources/com/ajaxjs/nativescript/ios/HttpRequest.swift 提供原生 HttpRequest.get 和 HttpRequest.post,使用 URLSession 异步请求,默认超时 15 秒。支持自定义请求头;POST 的字符串正文原样编码为 UTF-8,未传 Content-Type 时默认 application/json; charset=utf-8,不自动序列化或解析 JSON。

HttpRequest.get("https://example.com/api/status", headers: ["Accept": "application/json"]) { status, body, error in
    if let error = error {
        /*
         * 在此处理请求失败,不记录凭据或响应正文。
         */
        return
    }
    /*
     * 在此处理成功的 UTF-8 正文 body 和 HTTP 状态码 status。
     */
}

HttpRequest.post("https://example.com/api/action", body: "{\"action\":\"stop\"}") { status, body, error in
    /*
     * 先检查 error,再使用 status 与 body。
     */
}

回调统一异步进入主线程。HTTP 2xx 为成功;非 2xx 返回错误、实际状态码和可解码的正文。无 HTTP 响应时状态码为 0;网络、取消、参数或非 UTF-8 解码失败均通过 error 返回,204 无正文时 body 为 ""。返回的 URLSessionDataTask 已自动启动,可调用 cancel;参数错误返回 nil 并异步回调。查询参数由调用方用 URLComponents 编码,不拼接未编码用户输入。

类的 ObjC 名称为 AjaxJSHttpRequest,选择子为 get:headers:timeout:completion: 和 post:body:headers:timeout:completion:;当前没有额外封装统一 TS 接口。HTTP 明文访问遵循宿主 ATS 配置,不会自动放宽传输安全限制或绕过 TLS 证书校验。

固定键用户 Token

Sources/com/ajaxjs/nativescript/ios/GetCredential.swift 提供 GetCredential.saveUserToken(_:refreshToken:)、getUserToken()、getUserRefreshToken(),使用 user_token 和 user_refresh_token 键名,与 Android 对应方法一致。原生 ObjC 名为 AjaxJSGetCredential,保存选择子为 saveUserToken:refreshToken:,NativeScript 方法名为 saveUserTokenRefreshToken。返回 CryptoResult,须先检查 error;保存成功 value=true,读取缺失 value=null。两项写入不是事务,Keychain 锁定或存储错误不会静默当作未找到。

推荐使用包入口统一导出的 saveUserToken/getUserToken/getUserRefreshToken Promise 方法,参数、清理及异常说明见 通用工具。此类还提供 MQTT 账号密码的固定键存取,不承担凭据网络请求。

包入口另提供 getUserTokenSync(): string | null、getUserRefreshTokenSync(): string | null,直接读取同一 Keychain 项,缺失返回 null,空字符串保留。Keychain 锁定、系统读取或桥接失败时同步抛异常;当前线程可能被阻塞。原有 Promise 方法保留,同步方法不等待异步保存,Cookie 示例见 通用工具。

MQTT 账号密码

GetCredential 与 Android 一致,提供 MQTT_USERNAME_KEY = "mqtt_username"、MQTT_PASSWORD_KEY = "mqtt_password"。setMqttCredential(_:password:) 依次写入,getMqttUsername()、getMqttPassword() 分别读取 Keychain。支持空值、Unicode、空格和下划线,不拆分或 Base64 解码、不请求网络。两项不是事务,失败应停止连接并重试完整保存。

ObjC 类 AjaxJSGetCredential,NativeScript 方法 setMqttCredentialPassword(username, password)、getMqttUsername()、getMqttPassword() 均返回 CryptoResult;先检查 error,保存成功 value=true,读取缺失 value=null。统一 TS 三方法及同步阻塞、清理规则见 通用工具。旧 getCredential 方法已移除,Token 接口保留。

删除及客户端认证 Token

AjaxJSGetCredential.removeUserToken()、removeMqttCredential() 分别删除两类安全项,未找到也成功,两项非事务;CLIENT_CREDENTIAL_KEY = "client_credential" 用于独立的客户端认证 Token。saveClientCredential(token)、getClientCredential()、removeClientCredential() 只操作该 Keychain 项。成功保存/删除 value=true,读取缺失 value=null,空字符串保留,失败 error。

用户保存选择子现为 saveUserToken:refreshToken:,NativeScript 方法名 saveUserTokenRefreshToken,旧 saveUserTokenKey 名称移除。Token 的异步与同步读取保留。统一 TS 客户端及删除方法为同步接口,须先完成在途保存再删除,说明见 通用工具。

AES 密钥字符串

AjaxJSGetCredential 增加 AES_KEY_KEY = "AES_KEY" 以及 saveAesKey(token)、getAesKey()、removeAesKey(),同步通过 SecureStorage 保存/读取/删除字符串。每项最多 65536 UTF-8 字节,允许空字符串;成功保存/删除 value=true,读取缺失 value=null,失败设置 error。无 Base64/AES 长度校验、不执行 HKDF 或导入别名;删除不会影响已有 AES 加解密密钥。参数、使用示例及与 importAesKey 的区别见 通用工具。