JSBox 学习路线:从入门到实践 (ChatGPT版)

JSBox 学习路线:从入门到实践 (ChatGPT版)

前言

这份学习材料根据我们之前互动学习的成果,并结合审阅意见修订而成。它旨在提供一个更准确、更详尽、更符合当前 JSBox API 最佳实践的系统化学习路径。JSBox 是一款强大的 iOS 脚本工具,它允许你使用 JavaScript 语言与 iOS 原生功能深度交互,实现自动化、自定义 UI 和各种实用工具。

本材料将按照以下八个主要阶段展开:

  1. 快速开始: 了解 JSBox 的基本哲学和代码运行方式。

  2. 基础接口: 掌握与应用、设备、网络和数据存储相关的核心 API。

  3. 构建界面: 学习如何使用 JavaScript 定义和布局原生 iOS UI。

  4. 控件列表: 深入探索 JSBox 提供的各种 UI 控件及其特定用法。

  5. 数据类型与内置函数: 理解 JavaScript 与 Native 数据转换,并掌握常用辅助函数。

  6. Promise 与高级特性: 学习异步编程的最佳实践,以及更多强大的扩展功能。

  7. 包管理: 了解如何组织大型、模块化、可维护的 JSBox 项目。

  8. Objective-C Runtime: 探索 JSBox 的终极武器,直接与 iOS 原生底层交互。

最后,我们将通过一个完整的“提醒应用小程序”案例来综合运用所有知识。


阶段一:快速开始 (Quick Start)

本阶段是 JSBox 学习的起点,旨在让你对 JSBox 的基本哲学、代码风格和运行方式有一个初步的认识。

核心概念

  • JavaScript 驱动: JSBox 脚本基于 JavaScript (支持 ES6 标准语法)。

  • API 风格: 所有 JSBox API 都以 开头(如开头(如ui, $http)。

  • 轻量化与移动优先: API 设计简洁,适合移动端编写。

  • 沙盒环境: 每个脚本在独立沙盒中运行。

  • 运行方式: App 内编写、URL Scheme 安装、VSCode 同步、AirDrop 传输等。

常用 API 示例

  1. 弹出简单提示
  
$ui.alert("Hello, JSBox Learner!");
  

  
$ui.alert({
  
  title: "欢迎学习",
  
  message: "这是你的第一个 JSBox 提示框。",
  
  actions: ["好的"]
  
});
  
  1. 打印日志到控制台
  
console.log("这是一个普通的日志信息。");
  
console.info("应用启动成功。");
  
console.warn("注意:某个参数可能为空。");
  
console.error("发生了一个错误!");
  
  1. 获取剪贴板内容并预览
  
const clipboardText = $clipboard.text;
  
if (clipboardText) {
  
  $ui.preview({ title: "剪贴板内容", text: clipboardText });
  
  $ui.toast("已获取剪贴板文本并预览。");
  
} else {
  
  $ui.toast("剪贴板中没有文本内容。");
  
}
  

  
const clipboardItems = $clipboard.items;
  
if (clipboardItems && clipboardItems.length > 0) {
  
  $ui.preview({
  
    title: "剪贴板所有项目",
  
    text: JSON.stringify(clipboardItems, null, 2)
  
  });
  
}
  

阶段二:基础接口 (Foundation APIs)

涵盖模块:app,app, device, http,http, cache, clipboard,clipboard, file, thread,thread, system, keychain,keychain, l10n。

常用 API 示例

  1. $app
  
console.log("JSBox 应用信息:", $app.info);
  
console.log("当前脚本运行环境:", $app.env);
  
$app.idleTimerDisabled = true;
  
$ui.toast("屏幕将保持常亮。");
  

  
$app.openURL("https://www.apple.com");
  
$app.openURL("weixin://");
  
  1. $device
  
const nt = $device.networkType;
  
const mapNetworkType = { 0: "无网络", 1: "Wi‑Fi", 2: "蜂窝" };
  
console.log("当前网络类型:", mapNetworkType[nt] ?? "未知");
  

  
const deviceInfo = $device.info;
  
$ui.alert({
  
  title: "设备信息",
  
  message: `
  
    型号: ${deviceInfo.model}
  
    系统版本: ${deviceInfo.version}
  
    屏幕宽度: ${deviceInfo.screen.width} px
  
    屏幕高度: ${deviceInfo.screen.height} px
  
    电池电量: ${(deviceInfo.battery.level * 100).toFixed(0)}%
  
    网络类型: ${(function () {
  
      const nt = $device.networkType;
  
      const map = { 0: "无网络", 1: "Wi‑Fi", 2: "蜂窝" };
  
      return map[nt] ?? "未知";
  
    })()}
  
    当前是否深色模式: ${$device.isDarkMode ? "是" : "否"}
  
  `
  
});
  

  
$device.taptic(0);
  
$ui.toast("设备轻微振动了一下。");
  
  1. $http
  
$ui.toast("将发起网络请求获取今日诗词...");
  
$ui.loading(true);
  

  
$http.get({
  
  url: "https://v1.hitokoto.cn/?c=a&c=b&c=c&c=d&c=e&c=f&c=g&c=h&c=i&c=j&c=k&encode=json",
  
  handler: function (resp) {
  
    $ui.loading(false);
  
    if (resp.error) {
  
      $ui.alert("请求失败: " + resp.error.localizedDescription);
  
      console.error("HTTP 请求错误:", resp.error);
  
      return;
  
    }
  
    const data = resp.data;
  
    if (data && data.hitokoto) {
  
      $ui.alert({ title: "一言", message: `${data.hitokoto}\n---- ${data.from}` });
  
      console.log("今日一言数据:", data);
  
    } else {
  
      $ui.alert("未获取到有效数据。");
  
    }
  
  }
  
});
  

  
// 下载图片并分享(可选)
  
// const imageUrl = $clipboard.link || "https://example.com/example.png";
  
// if (imageUrl) {
  
//   $ui.toast("将下载图片并尝试分享...");
  
//   $ui.loading(true);
  
//   $http.download({
  
//     url: imageUrl,
  
//     showsProgress: true,
  
//     message: "下载图片中...",
  
//     handler: function (resp) {
  
//       $ui.loading(false);
  
//       if (resp.error) return $ui.alert("图片下载失败: " + resp.error.localizedDescription);
  
//       if (resp.data) {
  
//         $share.sheet(resp.data);
  
//         $ui.toast("图片下载完成并已调起分享。");
  
//       }
  
//     }
  
//   });
  
// }
  
  1. $file(同步 API)
  
const filename = "my_temp_log.txt";
  
const foldername = "my_data_folder";
  

  
const writeSuccess = $file.write({
  
  data: $data({ string: "日志内容:" + new Date().toLocaleString() + "\n" }),
  
  path: filename
  
});
  
if (writeSuccess) {
  
  $ui.toast("文件写入成功: " + filename);
  
  console.log("绝对路径:", $file.absolutePath(filename));
  
} else {
  
  $ui.alert("文件写入失败!");
  
}
  

  
if ($file.exists(filename)) {
  
  const fileData = $file.read(filename);
  
  if (fileData && fileData.string) {
  
    $ui.alert({ title: "文件内容", message: fileData.string });
  
  }
  
}
  

  
if (!$file.exists(foldername)) {
  
  const ok = $file.mkdir(foldername);
  
  if (ok) $ui.toast("文件夹创建成功: " + foldername);
  
}
  

  
console.log("当前脚本沙盒内容:", $file.list("./"));
  
// $file.delete(filename);
  
  1. $cache(同步 API)
  
$cache.set("appSettings", { username: "JSBoxUser", theme: "dark", fontSize: 16 });
  
$ui.toast("设置已缓存!");
  
console.log("从缓存中读取:", $cache.get("appSettings"));
  
// $cache.clear();
  
  1. $clipboard
  
$ui.alert({ title: "当前剪贴板文本", message: $clipboard.text || "剪贴板为空" });
  
$clipboard.text = "这段文字是从 JSBox 设置的!";
  
$ui.toast("剪贴板文本已更新。");
  
console.log("剪贴板中的链接:", $clipboard.link);
  
console.log("剪贴板中的电话号码:", $clipboard.phoneNumber);
  
  1. $thread 与延时
  
$ui.loading(true);
  
$ui.toast("后台任务进行中...");
  

  
$delay(0.1, () => {
  
  $thread.background(() => {
  
    for (let i = 0; i < 10_000_000; i++) {}
  
    $thread.main(() => {
  
      $ui.loading(false);
  
      $ui.toast("后台任务完成!");
  
    });
  
  });
  
});
  

  
$delay(2, () => $ui.toast("2 秒后执行的任务。"));
  

  
(async () => {
  
  $ui.toast("等待 3 秒...");
  
  await $wait(3);
  
  $ui.alert("等待结束,弹出 Alert。");
  
})();
  
  1. $system(注意权限与系统限制)
  
// $system.brightness = 0.5;
  
// $system.volume = 0.8; // 可能受 iOS 限制而无效
  

  
// 拨打电话
  
// $system.call("10086");
  

  
// 短信与邮件(推荐)
  
/*
  
await $message.sms({ recipients: ["10086"], body: "你好,我想查询话费。" });
  
await $message.mail({ to: ["log.e@qq.com"], subject: "测试邮件", body: "这是来自 JSBox 的测试邮件。" });
  
*/
  

  
// mailto
  
// $app.openURL("mailto:xxx@example.com?subject=测试&body=来自JSBox");
  
  1. $keychain(同步 API)
  
const KEY = "mySecretPassword";
  
const DOMAIN = "com.myapp.domain";
  

  
const setOk = $keychain.set(KEY, "your_secure_password_123", DOMAIN);
  
if (setOk) $ui.toast("密码已安全存储。");
  
const pwd = $keychain.get(KEY, DOMAIN);
  
if (pwd) $ui.alert({ title: "获取到密码", message: pwd });
  
// $keychain.remove(KEY, DOMAIN);
  
  1. l10nstrings/优先于l10n(strings/ 优先于 app.strings)
  
$app.strings = {
  
  "en": { "APP_NAME": "My Applet", "GREETING": "Hello, how are you?", "OK": "OK" },
  
  "zh-Hans": { "APP_NAME": "我的小程序", "GREETING": "你好,你好吗?", "OK": "好的" }
  
};
  

  
$ui.alert({
  
  title: $l10n("APP_NAME"),
  
  message: $l10n("GREETING"),
  
  actions: [$l10n("OK")]
  
});
  

阶段三:构建界面 (Build UI)

核心概念

  • UI 树结构,ui.render/ui.render/ui.push。

  • props/layout/events。

  • $(id) 获取视图实例。

  • Dark Mode 适配:$color({ light, dark })。

  • 修改当前页面背景:ui.controller.view.bgcolorui.controller.view.bgcolor 或 ui.window.bgcolor。

示例

  1. 基本页面
  
$ui.render({
  
  props: {
  
    title: "我的第一个 UI 页面",
  
    navBarHidden: false,
  
    bgcolor: $color("systemBackground"),
  
    theme: "auto"
  
  },
  
  views: [
  
    {
  
      type: "label",
  
      props: {
  
        text: "Hello, UI World!",
  
        textColor: $color("label"),
  
        font: $font("bold", 28),
  
        align: $align.center
  
      },
  
      layout: (make, view) => {
  
        make.centerX.equalTo(view.super);
  
        make.centerY.equalTo(view.super).offset(-50);
  
        make.width.equalTo(view.super).multipliedBy(0.8);
  
        make.height.equalTo(50);
  
      },
  
      events: {
  
        tapped: () => $ui.toast("你点击了标签!")
  
      }
  
    }
  
  ]
  
});
  
  1. 页面跳转
  
$ui.render({
  
  props: { title: "主页面" },
  
  views: [
  
    {
  
      type: "button",
  
      props: { title: "进入详情" },
  
      layout: $layout.center,
  
      events: {
  
        tapped: () => {
  
          $ui.push({
  
            props: { title: "详情页面", bgcolor: $color("systemBackground") },
  
            views: [{ type: "label", props: { text: "这是详情内容", align: $align.center }, layout: $layout.fill }]
  
          });
  
        }
  
      }
  
    }
  
  ]
  
});
  
  1. 动态修改视图
  
$ui.render({
  
  views: [
  
    {
  
      type: "label",
  
      props: {
  
        id: "myDynamicLabel",
  
        text: "点击按钮改变我!",
  
        textColor: $color("label"),
  
        font: $font(20),
  
        align: $align.center
  
      },
  
      layout: (make) => {
  
        make.centerX.equalTo(make.super);
  
        make.top.inset(100);
  
        make.width.equalTo(250);
  
        make.height.equalTo(40);
  
      }
  
    },
  
    {
  
      type: "button",
  
      props: { title: "改变文本" },
  
      layout: (make) => {
  
        make.centerX.equalTo(make.super);
  
        make.top.equalTo($("myDynamicLabel").bottom).offset(30);
  
        make.size.equalTo($size(120, 40));
  
      },
  
      events: {
  
        tapped: () => {
  
          const label = $("myDynamicLabel");
  
          label.text = "文本已改变!" + new Date().getSeconds();
  
          label.textColor = $color($rgb(Math.random() * 255, Math.random() * 255, Math.random() * 255));
  
          $ui.toast("文本已更新!");
  
        }
  
      }
  
    }
  
  ]
  
});
  
  1. 复杂布局
  
$ui.render({
  
  views: [
  
    {
  
      type: "view",
  
      props: { id: "redView", bgcolor: $color("red"), cornerRadius: 10 },
  
      layout: (make) => {
  
        make.left.top.inset(20);
  
        make.width.equalTo(100);
  
        make.height.equalTo(100);
  
      }
  
    },
  
    {
  
      type: "view",
  
      props: { id: "blueView", bgcolor: $color("blue"), cornerRadius: 10 },
  
      layout: (make) => {
  
        make.top.equalTo($("redView").top);
  
        make.left.equalTo($("redView").right).offset(10);
  
        make.size.equalTo($("redView"));
  
      }
  
    },
  
    {
  
      type: "view",
  
      props: { bgcolor: $color("green"), cornerRadius: 10 },
  
      layout: (make, view) => {
  
        make.top.equalTo($("blueView").bottom).offset(10);
  
        make.right.inset(20);
  
        make.width.equalTo(view.super).multipliedBy(0.5).offset(-20);
  
        make.height.equalTo(50);
  
      }
  
    }
  
  ]
  
});
  
  1. 手势与事件
  
$ui.render({
  
  views: [
  
    {
  
      type: "view",
  
      props: { bgcolor: $color("purple"), userInteractionEnabled: true },
  
      layout: $layout.center,
  
      events: {
  
        longPressed: (info) => $ui.toast(`长按!(${info.location.x.toFixed(0)}, ${info.location.y.toFixed(0)})`),
  
        doubleTapped: (sender) => {
  
          sender.alpha = sender.alpha === 1 ? 0.5 : 1;
  
          $ui.toast("双击:透明度切换。");
  
        },
  
        touchesBegan: (s, loc, locs) => console.log("触摸开始,触点数:", locs.length),
  
        touchesEnded: () => console.log("触摸结束。")
  
      }
  
    }
  
  ]
  
});
  
  1. 菜单(上下文与下拉)
  
$ui.render({
  
  props: {
  
    title: "菜单示例",
  
    navButtons: [
  
      {
  
        symbol: "ellipsis.circle",
  
        tintColor: $color("tintColor"),
  
        menu: {
  
          title: "更多操作",
  
          pullDown: true,
  
          items: [
  
            { title: "刷新", symbol: "arrow.clockwise", handler: () => $ui.toast("刷新中...") },
  
            { title: "分享", symbol: "square.and.arrow.up", handler: () => $share.sheet("分享内容") },
  
            {
  
              title: "高级",
  
              items: [
  
                { title: "设置", symbol: "gearshape", handler: () => $ui.toast("打开设置") },
  
                { title: "关于", symbol: "info.circle", handler: () => $ui.toast("关于脚本") }
  
              ]
  
            },
  
            { title: "删除数据", symbol: "trash", destructive: true, handler: () => $ui.alert("确认删除?") }
  
          ]
  
        }
  
      }
  
    ]
  
  },
  
  views: [
  
    {
  
      type: "label",
  
      props: {
  
        text: "长按我查看上下文菜单\n或点击导航栏按钮",
  
        align: $align.center,
  
        lines: 0,
  
        textColor: $color("label"),
  
        menu: {
  
          title: "上下文操作",
  
          items: [
  
            { title: "复制", symbol: "doc.on.doc", handler: () => { $clipboard.text = "复制的内容"; $ui.toast("已复制"); } },
  
            { title: "分享", symbol: "square.and.arrow.up", handler: () => $share.sheet("通过上下文菜单分享的内容") }
  
          ]
  
        }
  
      },
  
      layout: $layout.center
  
    }
  
  ]
  
});
  

阶段四:控件列表 (Components)

  1. label
  
{
  
  type: "label",
  
  props: {
  
    text: "这是一个标签",
  
    font: $font("ChalkboardSE-Bold", 24),
  
    textColor: $color("red"),
  
    shadowColor: $color("systemGray"),
  
    align: $align.left,
  
    lines: 2
  
  },
  
  layout: $layout.fill
  
}
  
  1. button
  
{
  
  type: "button",
  
  props: {
  
    title: "点击我",
  
    titleColor: $color("white"),
  
    bgcolor: $color("blue"),
  
    icon: $icon("007", $color("yellow"), $size(20, 20)),
  
    symbol: "folder.fill",
  
    imageEdgeInsets: $insets(0, 0, 0, 10)
  
  },
  
  events: { tapped: () => $ui.toast("按钮被点击了!") },
  
  layout: $layout.center
  
}
  
  1. input
  
{
  
  type: "input",
  
  props: {
  
    placeholder: "请输入文本",
  
    type: $kbType.default,
  
    darkKeyboard: true,
  
    text: "初始文本",
  
    secure: true
  
  },
  
  events: {
  
    changed: (sender) => console.log("输入变化:", sender.text),
  
    returned: (sender) => $ui.alert(`你输入了: ${sender.text}`)
  
  },
  
  layout: $layout.fill
  
}
  
  1. text
  
{
  
  type: "text",
  
  props: {
  
    placeholder: "请输入多行文本...",
  
    editable: true,
  
    selectable: true,
  
    insets: $insets(10, 10, 10, 10),
  
    styledText: "**粗体** *斜体* [链接](https://jsbox.app)",
  
    font: $font(16)
  
  },
  
  events: {
  
    didChange: (sender) => console.log("文本变化:", sender.text),
  
    didChangeSelection: (sender) => console.log("选中区域变化:", sender.selectedRange)
  
  },
  
  layout: $layout.fill
  
}
  
  1. list
  
{
  
  type: "list",
  
  props: {
  
    id: "list",
  
    rowHeight: 70,
  
    autoRowHeight: false,
  
    separatorHidden: false,
  
    separatorColor: $color("separatorColor"),
  
    reorder: true,
  
    data: [
  
      {
  
        title: "水果",
  
        rows: [
  
          { label: { text: "苹果" }, icon: { symbol: "apple.logo" } },
  
          { label: { text: "香蕉" }, icon: { symbol: "leaf.fill" } }
  
        ]
  
      },
  
      {
  
        title: "动物",
  
        rows: [
  
          { label: { text: "猫" }, icon: { symbol: "pawprint.fill" } },
  
          { label: { text: "狗" }, icon: { symbol: "hare.fill" } }
  
        ]
  
      }
  
    ],
  
    template: {
  
      views: [
  
        {
  
          type: "image",
  
          props: { id: "icon", tintColor: $color("tintColor") },
  
          layout: (make) => { make.left.inset(15); make.centerY.equalTo(make.super); make.size.equalTo($size(30, 30)); }
  
        },
  
        {
  
          type: "label",
  
          props: { id: "label" },
  
          layout: (make) => { make.left.equalTo($("icon").right).offset(10); make.right.inset(15); make.centerY.equalTo(make.super); }
  
        }
  
      ]
  
    },
  
    actions: [
  
      {
  
        title: "删除",
  
        color: $color("red"),
  
        handler: (sender, indexPath) => sender.delete(indexPath)
  
      },
  
      {
  
        title: "编辑",
  
        color: $color("blue"),
  
        handler: (sender, indexPath) => $ui.toast(`编辑 ${sender.data[indexPath.section].rows[indexPath.row].label.text}`)
  
      }
  
    ]
  
  },
  
  events: {
  
    didSelect: (sender, indexPath, data) => $ui.toast(`选择了: ${data.label.text}`),
  
    reorderMoved: (fromPath, toPath) => console.log(`从 ${fromPath.section}-${fromPath.row} 到 ${toPath.section}-${toPath.row}`),
  
    pulled: (sender) => {
  
      $ui.toast("加载更多数据...");
  
      $ui.loading(true);
  
      $delay(2, () => {
  
        const currentData = $("list").data;
  
        const newData = Array(5).fill(0).map((_, i) => ({ label: { text: `新项目 ${currentData[0].rows.length + i + 1}` } }));
  
        currentData[0].rows.push(...newData);
  
        $("list").data = currentData;
  
        sender.endRefreshing();
  
        $ui.loading(false);
  
      });
  
    },
  
    didReachBottom: (sender) => {
  
      $ui.toast("加载更多数据...");
  
      $ui.loading(true);
  
      $delay(1.5, () => {
  
        const currentData = $("list").data;
  
        const newItems = Array(5).fill(0).map((_, i) => ({ label: { text: `更多项 ${currentData[0].rows.length + i + 1}` }, icon: { symbol: "cloud.fill" } }));
  
        currentData[0].rows.push(...newItems);
  
        $("list").data = currentData;
  
        sender.endFetchingMore();
  
        $ui.loading(false);
  
      });
  
    }
  
  },
  
  layout: $layout.fill
  
}
  
  1. matrix
  
{
  
  type: "matrix",
  
  props: {
  
    columns: 3,
  
    itemHeight: 120,
  
    spacing: 5,
  
    data: Array(10).fill(0).map((_, i) => ({
  
      label: { text: `Item ${i + 1}` },
  
      image: { symbol: `star.fill` }
  
    })),
  
    template: {
  
      views: [
  
        {
  
          type: "image",
  
          props: { id: "image", tintColor: $color("systemYellow") },
  
          layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(10); make.size.equalTo($size(60, 60)); }
  
        },
  
        {
  
          type: "label",
  
          props: { id: "label", align: $align.center, font: $font(14), textColor: $color("label") },
  
          layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("image").bottom).offset(5); make.width.equalTo(make.super); make.height.equalTo(20); }
  
        }
  
      ]
  
    }
  
  },
  
  events: { didSelect: (sender, indexPath, data) => $ui.toast(`点击了: ${data.label.text}`) },
  
  layout: $layout.fill
  
}
  
  1. web
  
{
  
  type: "web",
  
  props: {
  
    url: "https://www.apple.com/cn/",
  
    showsProgress: true,
  
    toolbar: true,
  
    scrollEnabled: true,
  
    script: function () {
  
      const heading = document.querySelector('h1');
  
      if (heading) $notify("webContentLoaded", { text: heading.innerText });
  
    }
  
  },
  
  events: {
  
    didFinish: () => $ui.toast("网页加载完成!"),
  
    didFail: (sender, navigation, error) => $ui.alert("网页加载失败: " + error.localizedDescription),
  
    decideNavigation: (sender, action) => {
  
      // 若取不到 requestURL,可尝试 action.URL 或 action.url,按当前文档为准
  
      if (action.requestURL && action.requestURL.startsWith("https://apple.com/cn/iphone/")) return false;
  
      return true;
  
    },
  
    webContentLoaded: (object) => $ui.alert(`来自网页的消息:${object.text}`)
  
  },
  
  layout: $layout.fill
  
}
  
  1. picker
  
$ui.render({
  
  views: [
  
    {
  
      type: "button",
  
      props: { title: "选择日期" },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(120, 40)); },
  
      events: {
  
        tapped: async () => {
  
          const selectedDate = await $picker.date({ mode: 0, date: new Date() });
  
          if (selectedDate) $ui.alert(`你选择了日期: ${selectedDate.toLocaleString()}`);
  
          else $ui.toast("取消日期选择。");
  
        }
  
      }
  
    },
  
    {
  
      type: "button",
  
      props: { title: "选择颜色" },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(120, 40)); },
  
      events: {
  
        tapped: async () => {
  
          const selectedColor = await $picker.color({ color: $color("red") });
  
          if (selectedColor) {
  
            $ui.alert(`你选择了颜色: ${selectedColor.hexCode}`);
  
            $ui.controller.view.bgcolor = selectedColor;
  
          } else {
  
            $ui.toast("取消颜色选择。");
  
          }
  
        }
  
      }
  
    },
  
    {
  
      type: "button",
  
      props: { title: "通用选择器" },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(150, 40)); },
  
      events: {
  
        tapped: async () => {
  
          const result = await $picker.data({ items: [["选项 A1", "选项 A2", "选项 A3"], ["选项 B1", "选项 B2"]] });
  
          if (result) {
  
            $ui.alert(`你选择了: ${result.data[0]} 和 ${result.data[1]} (索引: ${result.selectedRows[0]}, ${result.selectedRows[1]})`);
  
          } else {
  
            $ui.toast("取消通用选择。");
  
          }
  
        }
  
      }
  
    }
  
  ]
  
});
  

阶段五:数据类型与内置函数 (Data Types & Built-in Functions)

核心概念

  • 数据类型构造:rect,rect, size, point,point, insets, color,color, font, data,data, image, icon,icon, indexPath 等。

  • 动态适配:color(light,dark)/color({ light, dark })/image({ light, dark })。

  • 全局常量:align,align, env, blurStyle,blurStyle, contentMode, kbType,kbType, mediaType, alertActionType,alertActionType, popoverDirection 等。

  • 辅助函数:l10n,l10n, delay, wait,wait, props, $desc 等。

示例

  1. 几何与颜色
  
$ui.render({
  
  props: { title: "几何与颜色", bgcolor: $color("systemBackground"), theme: "auto" },
  
  views: [
  
    {
  
      type: "view",
  
      props: {
  
        frame: $rect(20, 20, 100, 100),
  
        bgcolor: $rgba(255, 0, 0, 0.7),
  
        cornerRadius: 10,
  
        borderWidth: 2,
  
        borderColor: $color({ light: "#0000FF", dark: "#00FFFF" })
  
      }
  
    },
  
    {
  
      type: "label",
  
      props: { text: "动态颜色文本", textColor: $color("label"), font: $font("bold", 20) },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(150); }
  
    }
  
  ]
  
});
  
  1. data/data/image/$icon
  
const base64String = "SGVsbG8sIFdvcmxkIQ==";
  
const textData = $data({ base64: base64String, encoding: 4 });
  
console.log("Base64 解码文本:", textData.string);
  

  
const transparentGifBase64 = "R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
  
$ui.render({
  
  views: [
  
    {
  
      type: "image",
  
      props: {
  
        image: $image("data:image/gif;base64," + transparentGifBase64),
  
        symbol: "person.circle.fill",
  
        tintColor: $color("systemGreen"),
  
        contentMode: $contentMode.scaleAspectFit
  
      },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(80, 80)); }
  
    },
  
    {
  
      type: "label",
  
      props: { icon: $icon("005", $color("systemGreen"), $size(24, 24)), text: "  JSBox 内置图标", font: $font(18), textColor: $color("label") },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("image").bottom).offset(20); }
  
    }
  
  ]
  
});
  
  1. indexPath/indexPath/range
  
const p = $indexPath(1, 5);
  
console.log(`区: ${p.section}, 行: ${p.row}`);
  
const r = $range(10, 5);
  
console.log(`范围起始: ${r.location}, 长度: ${r.length}`);
  
  1. 延时
  
$ui.toast("将在 1 秒后消失...");
  
$delay(1, () => $ui.alert("提示已消失。"));
  

  
(async () => {
  
  $ui.toast("等待 2 秒...");
  
  await $wait(2);
  
  $ui.alert("等待结束,弹出 Alert。");
  
})();
  

阶段六:Promise 与高级特性 (Promise & Advanced Features)

核心概念

  • Promise/async-await。

  • JSBox API 的 required/optional 两种 Promise 模式(optional 需 async: true)。

  • 扩展 API:text,text, qrcode, archiver,archiver, browser, detector,detector, share, $push。

  • 原生 SDK:calendar,calendar, reminder, contact,contact, location, photo,photo, message, $safari。

示例

  1. 异步链
  
$ui.render({
  
  props: { title: "Async/Await 演示", theme: "auto" },
  
  views: [
  
    {
  
      type: "button",
  
      props: { title: "开始异步链" },
  
      layout: $layout.center,
  
      events: {
  
        tapped: async () => {
  
          $ui.loading(true);
  
          $ui.toast("步骤 1/3: 请求数据...");
  
          try {
  
            const httpResp = await $http.get("https://fanyi.youdao.com/openapi.do?keyfrom=JSBox_Test&key=139365261&type=data&doctype=json&version=1.1&q=hello");
  
            $ui.loading(false);
  
            if (httpResp.error) throw new Error("网络请求失败: " + httpResp.error.localizedDescription);
  
            const translation = httpResp.data.translation[0];
  
            $ui.toast("步骤 1 完成:翻译成功!");
  
            $ui.loading(true);
  
            $ui.toast("步骤 2/3: 请输入文本,结果将 Base64 编码...");
  

  
            const inputText = await $input.text({ placeholder: "请输入一个短语进行 Base64 编码" });
  
            $ui.loading(false);
  
            if (!inputText) return $ui.toast("取消输入,停止操作。");
  

  
            const encodedText = $text.base64Encode(inputText);
  
            $ui.toast("步骤 2 完成:文本已编码!");
  
            $ui.loading(true);
  
            $ui.toast("步骤 3/3: 请从相册选取一张图片...");
  

  
            const photoResp = await $photo.pick({ mediaTypes: [$mediaType.image], async: true });
  
            $ui.loading(false);
  

  
            if (photoResp && photoResp.image) {
  
              $ui.alert({
  
                title: "所有步骤完成!",
  
                message: `
  
                  原文: ${inputText} -> 翻译: ${translation}
  
                  编码后: ${encodedText}
  
                  图片选取成功!
  
                `
  
              });
  
            } else {
  
              $ui.toast("步骤 3 取消或未选取图片。");
  
            }
  
          } catch (e) {
  
            $ui.loading(false);
  
            $ui.alert("操作失败: " + e.message);
  
            console.error("异步链错误:", e);
  
          }
  
        }
  
      }
  
    }
  
  ]
  
});
  
  1. $text
  
console.log("UUID:", $text.uuid);
  
const originalUrl = "https://www.example.com?param=中文内容&key=value";
  
console.log("URL 编码:", $text.URLEncode(originalUrl));
  
console.log("URL 解码:", $text.URLDecode($text.URLEncode(originalUrl)));
  
console.log("MD5:", $text.MD5("JSBox"));
  
console.log("SHA1:", $text.SHA1("JSBox"));
  
console.log("SHA256:", $text.SHA256("JSBox"));
  

  
const markdownText = "# 标题\n**粗体文本** *斜体文本*";
  
const htmlText = "<h1>标题</h1><b>粗体文本</b><i>斜体文本</i>";
  
const convertedHtml = $text.markdownToHtml(markdownText);
  
console.log("Markdown 转 HTML:", convertedHtml);
  

  
(async () => {
  
  const markdown = await $text.htmlToMarkdown({ html: htmlText });
  
  console.log("HTML 转 Markdown:", markdown);
  
})();
  
  1. $qrcode
  
$ui.render({
  
  props: { title: "二维码示例" },
  
  views: [
  
    {
  
      type: "image",
  
      props: { id: "qrcodeImage", image: $qrcode.encode("https://jsbox.app"), contentMode: $contentMode.scaleAspectFit },
  
      layout: (make) => { make.center.equalTo(make.super); make.size.equalTo($size(150, 150)); }
  
    },
  
    {
  
      type: "button",
  
      props: { title: "扫描二维码" },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("qrcodeImage").bottom).offset(30); make.size.equalTo($size(120, 40)); },
  
      events: {
  
        tapped: async () => {
  
          const scanResult = await $qrcode.scan();
  
          if (scanResult) $ui.alert(`扫描结果: ${scanResult}`);
  
          else $ui.toast("取消扫描。");
  
        }
  
      }
  
    }
  
  ]
  
});
  
  1. $archiver
  
$file.write({ data: $data({ string: "Hello world for zip test!" }), path: "test.txt" });
  
$ui.loading(true);
  
$ui.toast("文件压缩中...");
  
$archiver.zip({
  
  paths: ["test.txt"],
  
  dest: "myarchive.zip",
  
  handler: async (success) => {
  
    $ui.loading(false);
  
    if (success) {
  
      $ui.toast("文件压缩成功!");
  
      $ui.loading(true);
  
      $ui.toast("文件解压中...");
  
      const unzipSuccess = await $archiver.unzip({ path: "myarchive.zip", dest: "unzipped_folder" });
  
      $ui.loading(false);
  
      if (unzipSuccess) {
  
        $ui.toast("文件解压成功!");
  
        console.log("解压后的文件:", $file.list("unzipped_folder"));
  
      } else {
  
        $ui.alert("文件解压失败!");
  
      }
  
    } else {
  
      $ui.alert("文件压缩失败!");
  
    }
  
  }
  
});
  
  1. Native SDK(photo/photo/calendar)
  
$ui.render({
  
  props: { title: "Native SDK 示例" },
  
  views: [
  
    {
  
      type: "button",
  
      props: { title: "选取照片" },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(120, 40)); },
  
      events: {
  
        tapped: async () => {
  
          $ui.loading(true);
  
          try {
  
            const resp = await $photo.pick({ mediaTypes: [$mediaType.image], async: true });
  
            $ui.loading(false);
  
            if (resp && resp.image) $ui.alert("选取照片成功!");
  
            else $ui.toast("未选取照片或取消。");
  
          } catch (e) {
  
            $ui.loading(false);
  
            $ui.alert("选取照片失败: " + e.message);
  
          }
  
        }
  
      }
  
    },
  
    {
  
      type: "button",
  
      props: { title: "创建日历事件" },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(150, 40)); },
  
      events: {
  
        tapped: async () => {
  
          $ui.loading(true);
  
          try {
  
            const now = new Date();
  
            const tomorrow = new Date(now.getTime() + 24 * 3600 * 1000);
  
            const resp = await $calendar.create({
  
              title: "JSBox 学习任务",
  
              startDate: now,
  
              endDate: tomorrow,
  
              notes: "完成 JSBox 学习材料的阅读和实践。",
  
              url: "https://jsbox.app",
  
              async: true
  
            });
  
            $ui.loading(false);
  
            if (resp && resp.status) $ui.toast("日历事件创建成功!");
  
            else $ui.alert("日历事件创建失败或取消。");
  
          } catch (e) {
  
            $ui.loading(false);
  
            $ui.alert("创建日历事件失败: " + e.message);
  
          }
  
        }
  
      }
  
    }
  
  ]
  
});
  

阶段七:包管理 (Package Management)

项目结构示例

  
MyPackageScript/
  
├── main.js
  
├── config.json
  
├── strings/
  
│   ├── en.strings
  
│   └── zh-Hans.strings
  
├── scripts/
  
│   └── data_manager.js
  
└── assets/
  
    └── icon.png
  
  1. main.js
  
// main.js
  
const dataManager = require('./scripts/data_manager');
  

  
$app.strings = {
  
  "en": { "APP_TITLE": "My Applet", "WELCOME_MSG": "Welcome to Applet!", "LOAD_DATA": "Load Data", "SAVE_DATA": "Save Data", "COUNT": "Count: ", "OK": "OK" },
  
  "zh-Hans": { "APP_TITLE": "我的小程序", "WELCOME_MSG": "欢迎使用小程序!", "LOAD_DATA": "加载数据", "SAVE_DATA": "保存数据", "COUNT": "数量:", "OK": "好的" }
  
};
  

  
$ui.render({
  
  props: { title: $l10n("APP_TITLE"), bgcolor: $color("systemBackground"), theme: "auto" },
  
  views: [
  
    {
  
      type: "image",
  
      props: { id: "image", src: "assets/icon.png", contentMode: $contentMode.scaleAspectFit },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(80, 80)); }
  
    },
  
    {
  
      type: "label",
  
      props: { id: "countLabel", text: $l10n("COUNT") + "0", align: $align.center, font: $font(20), textColor: $color("label") },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("image").bottom).offset(30); make.width.equalTo(250); make.height.equalTo(30); }
  
    },
  
    {
  
      type: "button",
  
      props: { title: $l10n("LOAD_DATA") },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("countLabel").bottom).offset(30); make.size.equalTo($size(150, 40)); },
  
      events: {
  
        tapped: async () => {
  
          $ui.loading(true);
  
          const data = await dataManager.loadData();
  
          $ui.loading(false);
  
          $("countLabel").text = $l10n("COUNT") + data.length;
  
          $ui.toast("数据加载完成!");
  
        }
  
      }
  
    },
  
    {
  
      type: "button",
  
      props: { title: $l10n("SAVE_DATA") },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(150, 40)); },
  
      events: {
  
        tapped: async () => {
  
          $ui.loading(true);
  
          const currentData = await dataManager.loadData();
  
          currentData.push({ timestamp: Date.now() });
  
          const success = await dataManager.saveData(currentData);
  
          $ui.loading(false);
  
          if (success) {
  
            $("countLabel").text = $l10n("COUNT") + currentData.length;
  
            $ui.toast("数据保存成功!");
  
          } else {
  
            $ui.alert("数据保存失败!");
  
          }
  
        }
  
      }
  
    }
  
  ]
  
});
  

  
(async () => {
  
  const initialData = await dataManager.loadData();
  
  $("countLabel").text = $l10n("COUNT") + initialData.length;
  
})();
  
  1. scripts/data_manager.js
  
// scripts/data_manager.js
  
const DATA_FILE = "my_app_data.json";
  

  
async function loadData() {
  
  if (!$file.exists(DATA_FILE)) return [];
  
  try {
  
    const data = $file.read(DATA_FILE);
  
    if (data && data.string) return JSON.parse(data.string);
  
  } catch (e) {
  
    console.error("Error parsing data file:", e);
  
    $ui.alert("加载数据失败!");
  
  }
  
  return [];
  
}
  

  
async function saveData(data) {
  
  try {
  
    const dataString = JSON.stringify(data, null, 2);
  
    const success = $file.write({ data: $data({ string: dataString, encoding: 4 }), path: DATA_FILE });
  
    return success;
  
  } catch (e) {
  
    console.error("Error saving data file:", e);
  
    $ui.alert("保存数据失败!");
  
    return false;
  
  }
  
}
  

  
module.exports = { loadData, saveData };
  
  1. strings/en.strings
  
"APP_TITLE" = "My Package App";
  
"WELCOME_MSG" = "Welcome!";
  
"LOAD_DATA" = "Load Data";
  
"SAVE_DATA" = "Save Data";
  
"COUNT" = "Count: ";
  
"OK" = "OK";
  
  1. strings/zh-Hans.strings
  
"APP_TITLE" = "我的打包应用";
  
"WELCOME_MSG" = "欢迎!";
  
"LOAD_DATA" = "加载数据";
  
"SAVE_DATA" = "保存数据";
  
"COUNT" = "数量:";
  
"OK" = "好的";
  

阶段八:Objective-C Runtime (Runtime)

核心概念

  • objc获取类/实例;.objc 获取类/实例;.method 调用方法。

  • jsValue()/ocValue() 在 JS 与 Native 之间转换。

  • define/define/delegate 动态定义类与委托。

  • $block 封装回调。

示例

  1. 调用原生方法
  
const UIColor = $objc("UIColor");
  
const UIApplication = $objc("UIApplication");
  
const NSURL = $objc("NSURL");
  

  
const app = UIApplication.$sharedApplication();
  
const url = NSURL.$URLWithString("https://www.google.com");
  
if (app.$canOpenURL(url)) {
  
  app.$openURL(url);
  
  $ui.toast("已尝试打开 Google");
  
} else {
  
  $ui.alert("无法打开 Google");
  
}
  

  
// const UIScreen = $objc("UIScreen");
  
// const screen = UIScreen.$mainScreen();
  
// const currentBrightness = screen.$brightness();
  
// $ui.alert(`当前屏幕亮度: ${(currentBrightness * 100).toFixed(0)}%`);
  
// screen.$setBrightness(0.5);
  
  1. 原生 Alert + Block
  
$ui.render({
  
  props: { title: "原生 Alert 演示" },
  
  views: [
  
    {
  
      type: "button",
  
      props: { title: "显示原生 Alert" },
  
      layout: $layout.center,
  
      events: {
  
        tapped: () => {
  
          const alertController = $objc("UIAlertController").$alertControllerWithTitle_message_preferredStyle_(
  
            "原生 Alert 标题",
  
            "这个 Alert 是通过 Objective-C Runtime API 创建的!",
  
            1
  
          );
  

  
          const defaultAction = $objc("UIAlertAction").$actionWithTitle_style_handler_(
  
            "好的",
  
            0,
  
            $block("void, id", () => $ui.toast("你点击了 '好的'。"))
  
          );
  
          const cancelAction = $objc("UIAlertAction").$actionWithTitle_style_handler_(
  
            "取消",
  
            1,
  
            $block("void, id", () => $ui.toast("你点击了 '取消'。"))
  
          );
  

  
          alertController.$addAction(defaultAction);
  
          alertController.$addAction(cancelAction);
  

  
          const currentVC = $ui.controller.ocValue();
  
          currentVC.$presentViewController_animated_completion_(alertController, true, null);
  
        }
  
      }
  
    }
  
  ]
  
});
  
  1. 混合视图(原生 UILabel)
  
$ui.render({
  
  props: { title: "原生标签混合显示", bgcolor: $color("systemBackground"), theme: "auto" },
  
  views: [
  
    {
  
      type: "label",
  
      props: { id: "jsLabel", text: "这是 JSBox UI 定义的标签", align: $align.center, font: $font("bold", 18), textColor: $color("label") },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.width.equalTo(280); make.height.equalTo(30); }
  
    },
  
    {
  
      type: "button",
  
      props: { id: "runtimeTargetButton", title: "点击查看 Runtime 效果", bgcolor: $color("systemOrange"), titleColor: $color("white"), cornerRadius: 8 },
  
      layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("jsLabel").bottom).offset(30); make.size.equalTo($size(200, 45)); }
  
    }
  
  ]
  
});
  

  
$thread.main(() => {
  
  const rootView = $ui.controller.view.ocValue();
  
  const nativeLabel = $objc("UILabel").$new();
  
  nativeLabel.$setText("这是由 Runtime 创建的原生标签");
  
  nativeLabel.$setTextColor($objc("UIColor").$systemBlueColor());
  
  nativeLabel.$setTextAlignment(1);
  
  nativeLabel.$setFont($objc("UIFont").$systemFontOfSize(18));
  

  
  const nativeLabelJSView = nativeLabel.jsValue();
  
  nativeLabelJSView.layout = (make) => {
  
    make.centerX.equalTo(make.super);
  
    make.top.equalTo($("runtimeTargetButton").bottom).offset(30);
  
    make.width.equalTo(300);
  
    make.height.equalTo(30);
  
  };
  
  rootView.$addSubview(nativeLabel);
  
});
  

综合应用案例:提醒应用小程序 (增强版)

结构

  
MyReminderApp/
  
├── main.js
  
├── config.json
  
├── strings/
  
│   ├── en.strings
  
│   └── zh-Hans.strings
  
├── scripts/
  
│   ├── reminder_model.js
  
│   ├── reminder_view.js
  
│   └── util.js
  
└── assets/
  
    └── icon.png
  
  1. config.json
  
{
  
  "info": {
  
    "name": "MyReminderApp",
  
    "version": "1.0.0",
  
    "author": "YourName",
  
    "icon": "icon.png",
  
    "category": "工具"
  
  },
  
  "settings": {
  
    "theme": "auto",
  
    "minSDKVer": "2.12.0"
  
  }
  
}
  
  1. strings/en.strings
  
"APP_TITLE" = "My Reminders";
  
"ADD_REMINDER" = "Add Reminder";
  
"EDIT_REMINDER" = "Edit Reminder";
  
"NEW_REMINDER" = "New Reminder";
  
"REMINDER_TEXT_PLACEHOLDER" = "Reminder Text";
  
"REMINDER_DATE" = "Reminder Date";
  
"SAVE" = "Save";
  
"DELETE" = "Delete";
  
"CANCEL" = "Cancel";
  
"REMINDER_SAVED_SUCCESS" = "Reminder saved!";
  
"REMINDER_DELETED_SUCCESS" = "Reminder deleted!";
  
"REMINDER_FETCH_FAILED" = "Failed to load reminders.";
  
"NO_REMINDERS" = "No reminders yet. Add one!";
  
"OK" = "OK";
  
"CONFIRM_DELETE_TITLE" = "Confirm Delete?";
  
"CONFIRM_DELETE_MESSAGE" = "Are you sure you want to delete this reminder?";
  
"NOTIFICATION_PERMISSION_DENIED" = "Notification permission denied. Cannot schedule reminder.";
  
"CALENDAR_PERMISSION_DENIED" = "Calendar permission denied. Cannot access calendar.";
  
"REMINDER_PERMISSION_DENIED" = "Reminders permission denied. Cannot access reminders.";
  
  1. strings/zh-Hans.strings
  
"APP_TITLE" = "我的提醒";
  
"ADD_REMINDER" = "添加提醒";
  
"EDIT_REMINDER" = "编辑提醒";
  
"NEW_REMINDER" = "新提醒";
  
"REMINDER_TEXT_PLACEHOLDER" = "提醒内容";
  
"REMINDER_DATE" = "提醒日期";
  
"SAVE" = "保存";
  
"DELETE" = "删除";
  
"CANCEL" = "取消";
  
"REMINDER_SAVED_SUCCESS" = "提醒已保存!";
  
"REMINDER_DELETED_SUCCESS" = "提醒已删除!";
  
"REMINDER_FETCH_FAILED" = "加载提醒失败。";
  
"NO_REMINDERS" = "暂无提醒,点击添加!";
  
"OK" = "好的";
  
"CONFIRM_DELETE_TITLE" = "确认删除?";
  
"CONFIRM_DELETE_MESSAGE" = "你确定要删除这条提醒吗?";
  
"NOTIFICATION_PERMISSION_DENIED" = "通知权限被拒绝。无法调度提醒。";
  
"CALENDAR_PERMISSION_DENIED" = "日历权限被拒绝。无法访问日历。";
  
"REMINDER_PERMISSION_DENIED" = "提醒事项权限被拒绝。无法访问提醒。";
  
  1. scripts/util.js(EventKit 权限包装)
  
// scripts/util.js
  

  
function handleError(message, error) {
  
  $ui.loading(false);
  
  $ui.alert(message);
  
  if (error) console.error("Error:", message, error);
  
  else console.error("Error:", message);
  
}
  

  
/**
  
 * 请求日历或提醒事项权限(EventKit)
  
 * @param {"calendar"|"reminder"} entity
  
 * @returns {Promise<boolean>}
  
 */
  
async function requestEventKitAccess(entity) {
  
  const EventStoreClass = $objc("EKEventStore");
  
  const store = EventStoreClass.$new();
  
  const type = entity === "reminder" ? 1 : 0; // 0: Event, 1: Reminder
  
  const status = EventStoreClass.$authorizationStatusForEntityType(type);
  

  
  if (status === 3) return true; // Authorized
  
  if (status === 2) { // Denied
  
    handleError(entity === "calendar" ? $l10n("CALENDAR_PERMISSION_DENIED") : $l10n("REMINDER_PERMISSION_DENIED"));
  
    return false;
  
  }
  
  if (status === 0 || status === 1) {
  
    return await new Promise((resolve) => {
  
      const completion = $block("void, BOOL, id", (granted, error) => {
  
        if (error) {
  
          console.error("EventKit permission error:", error);
  
          resolve(false);
  
        } else {
  
          resolve(!!granted);
  
        }
  
      });
  
      store.$requestAccessToEntityType_completion_(type, completion);
  
    });
  
  }
  
  return false;
  
}
  

  
module.exports = { handleError, requestEventKitAccess };
  
  1. scripts/reminder_model.js
  
// scripts/reminder_model.js
  
const util = require('./util');
  

  
const DATA_FILE = "reminders.json";
  
let remindersInMemory = [];
  

  
async function loadReminders() {
  
  if (!$file.exists(DATA_FILE)) {
  
    remindersInMemory = [];
  
    return [];
  
  }
  
  try {
  
    const data = $file.read(DATA_FILE);
  
    if (data && data.string) {
  
      remindersInMemory = JSON.parse(data.string);
  
      return remindersInMemory;
  
    }
  
  } catch (e) {
  
    console.error("Error parsing reminders.json:", e);
  
    util.handleError($l10n("REMINDER_FETCH_FAILED"), e);
  
  }
  
  remindersInMemory = [];
  
  return [];
  
}
  

  
async function saveReminders() {
  
  try {
  
    const dataString = JSON.stringify(remindersInMemory, null, 2);
  
    const success = $file.write({ data: $data({ string: dataString, encoding: 4 }), path: DATA_FILE });
  
    return success;
  
  } catch (e) {
  
    console.error("Error saving reminders.json:", e);
  
    util.handleError("保存提醒失败!", e);
  
    return false;
  
  }
  
}
  

  
async function addOrUpdateReminder(reminder, isEditing, index) {
  
  if (isEditing) remindersInMemory[index] = reminder;
  
  else remindersInMemory.push(reminder);
  
  return await saveReminders();
  
}
  

  
async function deleteReminder(index) {
  
  remindersInMemory.splice(index, 1);
  
  return await saveReminders();
  
}
  

  
function getRemindersInMemory() {
  
  return remindersInMemory;
  
}
  

  
// 调度通知:无需显式请求通知权限,iOS 首次调度会弹出授权弹窗。
  
// 若用户拒绝,后续调度可能静默失败,应引导去系统设置开启。
  
function scheduleNotification(reminder) {
  
  const when = new Date(reminder.date);
  
  if (when > new Date()) {
  
    $push.schedule({
  
      id: reminder.id,
  
      title: $l10n("APP_TITLE"),
  
      body: reminder.text,
  
      date: when,
  
      renew: true
  
    });
  
    console.log("Notification scheduled:", reminder.text);
  
  }
  
}
  

  
function cancelNotification(reminderId) {
  
  $push.cancel({ id: reminderId });
  
  console.log("Notification cancelled:", reminderId);
  
}
  

  
module.exports = {
  
  loadReminders,
  
  saveReminders,
  
  getRemindersInMemory,
  
  addOrUpdateReminder,
  
  deleteReminder,
  
  scheduleNotification,
  
  cancelNotification
  
};
  
  1. scripts/reminder_view.js
  
// scripts/reminder_view.js
  
const reminderModel = require('./reminder_model');
  
const util = require('./util');
  

  
async function renderMainView() {
  
  $ui.render({
  
    props: {
  
      title: $l10n("APP_TITLE"),
  
      bgcolor: $color("systemBackground"),
  
      theme: "auto",
  
      navButtons: [
  
        { symbol: "plus.circle", tintColor: $color("tintColor"), handler: () => pushEditView() }
  
      ]
  
    },
  
    views: [
  
      {
  
        type: "list",
  
        props: {
  
          id: "reminderList",
  
          rowHeight: 70,
  
          template: {
  
            views: [
  
              {
  
                type: "label",
  
                props: { id: "reminderText", font: $font(18), lines: 0, textColor: $color("label") },
  
                layout: (make) => { make.left.right.inset(15); make.top.inset(10); make.height.lessThanOrEqualTo(40); }
  
              },
  
              {
  
                type: "label",
  
                props: { id: "reminderDate", font: $font(13), textColor: $color("secondaryLabel") },
  
                layout: (make) => {
  
                  make.left.right.inset(15);
  
                  make.top.equalTo($("reminderText").bottom).offset(5);
  
                  make.height.equalTo(20);
  
                }
  
              },
  
              {
  
                type: "switch",
  
                props: { id: "completedSwitch", onColor: $color("systemGreen") },
  
                layout: (make) => { make.right.inset(15); make.centerY.equalTo(make.super); },
  
                events: {
  
                  changed: async (sender) => {
  
                    $ui.loading(true);
  
                    try {
  
                      const index = sender.info.index;
  
                      const reminders = reminderModel.getRemindersInMemory();
  
                      const reminder = reminders[index];
  
                      reminder.completed = sender.on;
  
                      const ok = await reminderModel.addOrUpdateReminder(reminder, true, index);
  
                      $ui.loading(false);
  
                      if (ok) {
  
                        $ui.toast(sender.on ? "已完成" : "未完成");
  
                        await loadRemindersToList();
  
                      } else {
  
                        $ui.alert("更新提醒失败。");
  
                        sender.on = !sender.on;
  
                      }
  
                    } catch (e) {
  
                      util.handleError("更新提醒状态失败", e);
  
                      sender.on = !sender.on;
  
                    }
  
                  }
  
                }
  
              }
  
            ]
  
          },
  
          actions: [
  
            { title: $l10n("DELETE"), color: $color("red"), handler: (sender, indexPath) => confirmDeleteReminder(indexPath.row) }
  
          ]
  
        },
  
        layout: $layout.fill,
  
        events: {
  
          didSelect: (sender, indexPath) => pushEditView(indexPath.row),
  
          pulled: async (sender) => { await loadRemindersToList(); sender.endRefreshing(); }
  
        }
  
      }
  
    ]
  
  });
  

  
  await loadRemindersToList();
  
  // 可选:在进入后台/退出时自动保存(需确认版本支持 $app.listen)
  
  // $app.listen({ pause: async () => await reminderModel.saveReminders(), exit: async () => await reminderModel.saveReminders() });
  
}
  

  
async function loadRemindersToList() {
  
  $ui.loading(true);
  
  await reminderModel.loadReminders();
  
  const reminders = reminderModel.getRemindersInMemory();
  
  $ui.loading(false);
  

  
  if (reminders.length === 0) {
  
    $("reminderList").data = [{ rows: [{ reminderText: { text: $l10n("NO_REMINDERS") }, completedSwitch: { hidden: true }, reminderDate: { hidden: true } }] }];
  
    $("reminderList").rowHeight = 100;
  
    $("reminderList").selectable = false;
  
  } else {
  
    const rows = reminders.map((r, i) => ({
  
      reminderText: { text: r.text, textColor: r.completed ? $color("secondaryLabel") : $color("label") },
  
      reminderDate: { text: new Date(r.date).toLocaleString(), textColor: r.completed ? $color("secondaryLabel") : $color("secondaryLabel") },
  
      completedSwitch: { on: r.completed, info: { index: i } },
  
      info: { id: r.id, index: i }
  
    }));
  
    $("reminderList").data = [{ rows }];
  
    $("reminderList").rowHeight = 70;
  
    $("reminderList").selectable = true;
  
  }
  
}
  

  
async function pushEditView(index) {
  
  let currentReminder, isEditing = false;
  
  const reminders = reminderModel.getRemindersInMemory();
  

  
  if (index !== undefined) {
  
    isEditing = true;
  
    currentReminder = { ...reminders[index] };
  
  } else {
  
    currentReminder = { id: Date.now().toString(), text: "", date: new Date().toISOString(), completed: false };
  
  }
  

  
  $ui.push({
  
    props: { title: isEditing ? $l10n("EDIT_REMINDER") : $l10n("NEW_REMINDER"), bgcolor: $color("systemBackground"), theme: "auto" },
  
    views: [
  
      {
  
        type: "input",
  
        props: { id: "reminderTextInput", placeholder: $l10n("REMINDER_TEXT_PLACEHOLDER"), text: currentReminder.text, font: $font(17), textColor: $color("label"), cornerRadius: 5, bgcolor: $color("secondarySystemBackground") },
  
        layout: (make) => { make.left.right.inset(15); make.top.inset(20); make.height.equalTo(40); }
  
      },
  
      {
  
        type: "label",
  
        props: { id: "reminderDate", text: $l10n("REMINDER_DATE"), textColor: $color("label"), font: $font(15) },
  
        layout: (make) => { make.left.inset(15); make.top.equalTo($("reminderTextInput").bottom).offset(20); }
  
      },
  
      {
  
        type: "date-picker",
  
        props: { id: "reminderDatePicker", mode: 0, date: new Date(currentReminder.date) },
  
        layout: (make) => { make.left.right.inset(15); make.top.equalTo($("reminderDate").bottom).offset(5); make.height.equalTo(180); }
  
      },
  
      {
  
        type: "button",
  
        props: { title: $l10n("SAVE"), bgcolor: $color("systemBlue"), titleColor: $color("white"), cornerRadius: 8 },
  
        layout: (make) => { make.left.right.inset(15); make.top.equalTo($("reminderDatePicker").bottom).offset(30); make.height.equalTo(45); },
  
        events: { tapped: () => saveReminder(currentReminder, isEditing, index) }
  
      },
  
      ...(isEditing ? [{
  
        type: "button",
  
        props: { title: $l10n("DELETE"), bgcolor: $color("systemRed"), titleColor: $color("white"), cornerRadius: 8 },
  
        layout: (make) => { make.left.right.inset(15); make.top.equalTo($("button").bottom).offset(15); make.height.equalTo(45); },
  
        events: { tapped: () => confirmDeleteReminder(index, true) }
  
      }] : [])
  
    ]
  
  });
  
}
  

  
async function saveReminder(reminder, isEditing, index) {
  
  const textInput = $("reminderTextInput").text;
  
  const datePicker = $("reminderDatePicker").date;
  

  
  if (!textInput.trim()) return $ui.alert($l10n("REMINDER_TEXT_PLACEHOLDER"));
  
  if (Number.isNaN(datePicker.getTime())) return $ui.alert("无效日期");
  

  
  reminder.text = textInput.trim();
  
  reminder.date = datePicker.toISOString();
  

  
  $ui.loading(true);
  
  try {
  
    const ok = await reminderModel.addOrUpdateReminder(reminder, isEditing, index);
  
    if (ok) {
  
      $ui.toast($l10n("REMINDER_SAVED_SUCCESS"));
  
      // 可选:调度本地通知
  
      // reminderModel.scheduleNotification(reminder);
  
      $ui.pop();
  
      await loadRemindersToList();
  
    } else {
  
      util.handleError("保存提醒失败!");
  
    }
  
  } catch (e) {
  
    util.handleError("保存提醒失败!", e);
  
  } finally {
  
    $ui.loading(false);
  
  }
  
}
  

  
async function confirmDeleteReminder(index, fromEditPage = false) {
  
  const confirm = await $ui.alert({
  
    title: $l10n("CONFIRM_DELETE_TITLE"),
  
    message: $l10n("CONFIRM_DELETE_MESSAGE"),
  
    actions: [{ title: $l10n("DELETE"), style: $alertActionType.destructive }, { title: $l10n("CANCEL") }]
  
  });
  
  if (confirm.index !== 0) return;
  

  
  $ui.loading(true);
  
  try {
  
    const ok = await reminderModel.deleteReminder(index);
  
    if (ok) {
  
      $ui.toast($l10n("REMINDER_DELETED_SUCCESS"));
  
      if (fromEditPage) $ui.pop();
  
      await loadRemindersToList();
  
    } else {
  
      util.handleError("删除提醒失败!");
  
    }
  
  } catch (e) {
  
    util.handleError("删除提醒失败!", e);
  
  } finally {
  
    $ui.loading(false);
  
  }
  
}
  

  
module.exports = { renderMainView };
  
  1. main.js
  
// main.js
  
const reminderView = require('./scripts/reminder_view');
  
reminderView.renderMainView();
  

综合实践

  • 打包并安装到 JSBox:将 MyReminderApp 压缩为 zip,分享到 iOS 用 JSBox 打开安装。

  • 运行后测试:添加/编辑/删除/完成状态切换/下拉刷新/本地化/暗黑模式适配。


学习总结与展望

  • 你已掌握:JSBox 核心 API、原生 UI 构建、模块化组织、异步编程、Runtime 交互。

  • 后续优化建议:

    • 权限管理:日历/提醒/照片/通知等,统一处理拒绝与异常。

    • 错误处理:统一 try-catch 与提示策略。

    • 状态管理:内存态 + 关键路径落盘,必要时生命周期补充。

    • 体验优化:loading 仅用于阻塞式等待,toast 用于结果提示。

    • 进阶功能:搜索过滤、重复提醒、本地通知、数据导入导出、快捷指令与 URL Scheme 集成。

    • 调试:充分利用控制台与 Safari 调试工具。