快速开始

欢迎使用 Scripting!这是一款可让你使用 TypeScript 编写 React 类似的 TSX 语法来创建 UI 组件和自定义小组件、灵动岛和使用发通知提醒等能力的应用。通过 Scripting,你可以使用包装过的 SwiftUI 视图来获得在 iOS 上流畅且原生的使用体验,并通过熟悉的编码结构来创建和呈现各种 iOS 工具型 UI 页面。本指南将带你完成项目设置、组件创建以及结合 Hooks 构建动态界面的流程。

目录

  1. 快速开始
  2. 创建脚本项目
  3. 导入组件
  4. 创建自定义组件
  5. 呈现 UI 视图
  6. 使用 Hooks
  7. 构建复杂的 UI

1. 快速开始

在 Scripting 中,你可以通过定义函数式组件的方式来创建简单的 UI 元素。你需要的所有组件和 API 都可以从 scripting 包里导入。

2. 创建脚本项目

在开始编写代码之前,你需要创建一个脚本项目。项目创建完成后,你可以在 index.tsx 文件中编写代码。这个文件是定义 UI 组件和逻辑的主要入口。

index.tsx 的示例:

1import { VStack, Text } from "scripting"
2
3// 定义一个自定义视图组件
4function View() {
5  return (
6    <VStack>
7      <Text>Hello, Scripting!</Text>
8    </VStack>
9  )
10}

3. 导入视图

SwiftUI 中的所有视图以及部分 API 都进行了包装,并通过 scripting 包提供给你使用。以下是部分可用视图的列表:

  • 布局视图: VStack, HStack, ZStack, Grid
  • 控件: Button, Picker, Toggle, Slider, ColorPicker
  • 集合: List, Section
  • 日期和时间: DatePicker
  • 文本和标签: Text, Label, TextField

你可以像这样在项目中导入它们:

1import { VStack, Text, Button, Picker } from "scripting"

4. 创建自定义组件

在 Scripting 中,函数式组件的工作原理与 React 基本相同,可以使用类似 JSX 的语法来构建可复用组件。

示例:

1import { VStack, HStack, Text, Button } from "scripting"
2
3function Greeting({
4   name
5}: {
6   name: string 
7}) {
8  return (
9    <HStack>
10      <Text>Hello, {name}!</Text>
11    </HStack>
12  )
13}
14
15function MainView() {
16  return (
17    <VStack>
18      <Greeting name="Scripting User" />
19      <Button 
20        title="Click Me" 
21        action={() => console.log("Button Clicked!")}
22      />
23    </VStack>
24  )
25}

5. 呈现 UI 视图

若要呈现 UI 视图,可以使用 Navigation.present 方法。它能够以模态视图的形式显示自定义组件,并处理该视图的关闭。Navigation.present 方法会返回一个在视图被关闭后才会完成的 Promise。为了避免内存泄漏,一定要在视图关闭后调用 Script.exit()

示例:

1import { VStack, Text, Navigation, Script } from "scripting"
2
3function View() {
4  return (
5    <VStack>
6      <Text>Hello, Scripting!</Text>
7    </VStack>
8  )
9}
10
11// 显示该视图
12Navigation.present({ 
13  element: <View />
14}).then(() => {
15  // 视图关闭后清理资源,避免内存泄漏
16  Script.exit()
17})

在上述示例中,Navigation.present({ element: <View /> }) 会呈现 View 组件;当用户关闭此视图后,Script.exit() 确保释放相关资源。


6. 使用 Hooks

Scripting 支持一系列与 React 类似的 Hooks,用于管理组件中的状态、副作用、Memo 化以及上下文。以下是每种 Hook 的使用指南及示例:


useState

useState Hook 能够让你在函数式组件中添加本地状态。

1import { useState, VStack, Text, Button } from "scripting"
2
3function Counter() {
4  const [count, setCount] = useState(0)
5
6  return (
7    <VStack>
8      <Text>Count: {count}</Text>
9      <Button
10        title="Increment"
11        action={() => setCount(count + 1)}
12      />
13    </VStack>
14  )
15}

在这个示例中,每次点击按钮都会更新 count 变量,并触发组件的自动重新渲染。


useEffect

useEffect Hook 可以让你在组件中执行副作用操作,比如获取数据或者设置订阅。

1import { useState, useEffect, VStack, Text } from "scripting"
2
3function TimeDisplay() {
4  const [time, setTime] = useState(
5    new Date().toLocaleTimeString()
6  )
7
8  useEffect(() => {
9    let timerId: number
10
11    const startTimer = () => {
12      timerId = setTimeout(() => {
13        setTime(new Date().toLocaleTimeString())
14      }, 1000)
15    }
16
17    startTimer()
18    
19    return () => clearTimeout(timerId) // 组件卸载时清理定时器
20  }, [])
21
22  return <Text>Current Time: {time}</Text>
23}

在此示例中,useEffect Hook 会设置一个间隔操作,每秒更新一次 time 变量,并在组件卸载时清除该间隔以避免潜在的问题。


useReducer

当你需要在组件中管理更复杂的状态逻辑时,useReducer Hook 非常有用。

1import { useReducer, VStack, Text, Button } from "scripting"
2
3type Action = { 
4  type: "increment"
5} | {
6  type: "decrement"
7}
8const reducer = (state: number, action: Action) => {
9  switch (action.type) {
10    case "increment":
11      return state + 1
12    case "decrement":
13      return state - 1
14    default:
15      return state
16  }
17}
18
19function Counter() {
20  const [count, dispatch] = useReducer(reducer, 0)
21
22  return (
23    <VStack>
24      <Text>Count: {count}</Text>
25      <Button 
26        title="Increment"
27        action={() => dispatch({ type: "increment" })}
28      />
29      <Button
30        title="Decrement"
31        action={() => dispatch({ type: "decrement" })}
32      />
33    </VStack>
34  )
35}

useReducer Hook 可以通过一个 reducer 函数来帮助你更好地处理复杂的状态变更。


useCallback

useCallback Hook 可以让你对函数进行 Memo 化,以避免在每次渲染时都重新创建函数,从而提升性能。

1import { useState, useCallback, VStack, Text, Button } from "scripting"
2
3function Counter() {
4  const [count, setCount] = useState(0)
5
6  const increment = useCallback(() => {
7    setCount((prev) => prev + 1)
8  }, [])
9
10  return (
11    <VStack>
12      <Text>Count: {count}</Text>
13      <Button 
14        title="Increment"
15        action={increment}
16      />
17    </VStack>
18  )
19}

使用 useCallback,只有在依赖项改变时才会重新创建 increment 函数,从而在大型或频繁更新的组件中提升性能。


useMemo

useMemo Hook 允许你对某些值进行 Memo 化,以缓存代价高的计算结果,从而提高性能。

1import { useState, useMemo, VStack, Text, Button } from "scripting"
2
3function FactorialCounter() {
4  const [count, setCount] = useState(1)
5
6  const factorial = useMemo(() => {
7    let result = 1
8    for (let i = 1; i <= count; i++) result *= i
9    return result
10  }, [count])
11
12  return (
13    <VStack>
14      <Text>Factorial of {count} is {factorial}</Text>
15      <Button 
16        title="Increase"
17        action={() => setCount(count + 1)}
18      />
19    </VStack>
20  )
21}

useMemo Hook 仅在 count 改变时才重新计算阶乘,从而避免不必要的性能消耗。


useContext

useContext Hook 允许你在应用的各组件之间共享状态,而无需进行层层的 props 传递(即“向下传递”)。

1import { createContext, useContext, VStack, Text, Button } from "scripting"
2
3const CountContext = createContext<number>()
4
5function Display() {
6  const count = useContext(CountContext)
7  return <Text>Shared Count: {count}</Text>
8}
9
10function App() {
11  return (
12    <CountContext.Provider value={42}>
13      <VStack>
14        <Display />
15      </VStack>
16    </CountContext.Provider>
17  )
18}

在此示例中,useContext 可以访问 CountContext,从而在应用中共享计数值。


7. 构建复杂的 UI

通过结合已提供的视图、Hooks 和自定义组件,你可以构建出功能完善、结构复杂的 UI。

示例:

1import { useState, VStack, Text, TextField, List, Section, NavigationStack, Script } from "scripting"
2
3function ToDoApp() {
4  const [tasks, setTasks] = useState(["Task 1", "Task 2", "Task 3"])
5  const [content, setContent] = useState("")
6
7  return (
8    <NavigationStack>
9        <List
10          navigationTitle="My Tasks"
11        >
12          <Section>
13            {tasks.map((task, index) => (
14              <Text key={index}>{task}</Text>
15            ))}
16          </Section>
17          
18          <TextField
19            title="New Task"
20            value={content}
21            onChanged={setContent}
22            onSubmit={() => {
23              if (content.length === 0) {
24                return
25              }
26              setTasks([...tasks, content])
27              setContent("")
28            }}
29          />
30        </List>
31    </NavigationStack>
32  )
33}
34
35async function run() {
36  await Navigation.present({
37    element: <ToDoApp />
38  })
39
40  Script.exit()
41}

如需了解更多详细信息,请查阅完整的 API 文档,该文档包含关于 scripting 包的更多示例和使用场景。