希望使用 Flutter 编写移动应用的 SwiftUI 开发者应查阅本指南。它解释了如何将现有的 SwiftUI 知识应用于 Flutter。

Flutter 是一个用于构建跨平台应用的框架,它使用 Dart 编程语言。要了解 Dart 和 Swift 编程之间的一些差异,请参阅Swift 开发者学习 DartSwift 开发者学习 Flutter 并发

在用 Flutter 构建应用时,你的 SwiftUI 知识和经验非常有价值。

Flutter 还在 iOS 和 macOS 上运行时对应用行为进行了一些适配。要了解如何操作,请参阅平台适配

本文档可以作为一本操作指南,你可以跳着阅读,找到与你的需求最相关的问题。本指南嵌入了示例代码。通过将鼠标悬停或聚焦时出现的“在 DartPad 中打开”按钮,你可以在 DartPad 上打开并运行一些示例。

概述

#

作为介绍,请观看以下视频。它概述了 Flutter 如何在 iOS 上工作以及如何使用 Flutter 构建 iOS 应用。

在新标签页中在 YouTube 上观看:“Flutter for iOS 开发者”

Flutter 和 SwiftUI 代码描述了 UI 的外观和工作方式。开发者将这种类型的代码称为*声明式框架*。

视图与组件

#

**SwiftUI** 将 UI 组件表示为*视图(views)*。你使用*修饰符(modifiers)*配置视图。

swift
Text("Hello, World!") // <-- This is a View
  .padding(10)        // <-- This is a modifier of that View

**Flutter** 将 UI 组件表示为*组件(widgets)*。

视图和组件都只存在到它们需要改变为止。这些语言将此属性称为*不可变性(immutability)*。SwiftUI 将 UI 组件属性表示为视图修饰符。相比之下,Flutter 将组件用于 UI 组件及其属性。

dart
Padding(                         // <-- This is a Widget
  padding: EdgeInsets.all(10.0), // <-- So is this
  child: Text("Hello, World!"),  // <-- This, too
)));

为了组合布局,SwiftUI 和 Flutter 都将 UI 组件相互嵌套。SwiftUI 嵌套视图,而 Flutter 嵌套组件。

布局过程

#

**SwiftUI** 使用以下过程布局视图:

  1. 父视图向其子视图建议一个尺寸。
  2. 所有后续的子视图
    • 向*它们*的子视图建议一个尺寸
    • 询问该子视图它想要什么尺寸
  3. 每个父视图以返回的尺寸渲染其子视图。

**Flutter** 的过程有所不同:

  1. 父组件将其约束条件传递给其子组件。约束条件包括高度和宽度的最小值和最大值。

  2. 子组件尝试决定其尺寸。它对其自己的子组件列表重复相同的过程:

    • 它将子组件的约束条件告知子组件。
    • 它询问子组件它希望的尺寸。
  3. 父组件布局子组件。

    • 如果请求的尺寸符合约束条件,父组件将使用该尺寸。
    • 如果请求的尺寸不符合约束条件,父组件将限制高度、宽度或两者以适应其约束条件。

Flutter 与 SwiftUI 不同,因为父组件可以覆盖子组件的期望尺寸。组件不能拥有它想要的任何尺寸。它也不能知道或决定其在屏幕上的位置,因为父组件做出了该决定。

要强制子组件以特定尺寸渲染,父组件必须设置严格约束。当其约束的最小尺寸值等于其最大尺寸值时,约束变得严格。

在 **SwiftUI** 中,视图可能会扩展到可用空间或将其尺寸限制为其内容的大小。**Flutter** 组件的行为方式相似。

然而,在 Flutter 中,父组件可以提供无界约束。无界约束将其最大值设置为无穷大。

dart
UnboundedBox(
  child: Container(
      width: double.infinity, height: double.infinity, color: red),
)

如果子组件扩展并且它具有无界约束,Flutter 会返回溢出警告。

dart
UnconstrainedBox(
  child: Container(color: red, width: 4000, height: 50),
)
When parents pass unbounded constraints to children, and the children are expanding, then there is an overflow warning.

要了解 Flutter 中约束如何工作,请参阅理解约束

设计系统

#

由于 Flutter 面向多个平台,你的应用无需符合任何设计系统。虽然本指南以 Material 组件为特色,但你的 Flutter 应用可以使用许多不同的设计系统:

  • 自定义 Material 组件
  • 社区构建的组件
  • 你自己的自定义组件
  • Cupertino 组件,遵循 Apple 的人机界面指南

在新标签页中在 YouTube 上观看:“Flutter 的 cupertino 库 for iOS 开发者”

如果你正在寻找一个具有自定义设计系统的出色参考应用,请查阅 Wonderous

UI 基础

#

本节涵盖了 Flutter 中 UI 开发的基础知识以及它与 SwiftUI 的比较。这包括如何开始开发你的应用、显示静态文本、创建按钮、响应点击事件、显示列表、网格等。

入门

#

在 **SwiftUI** 中,你使用 `App` 来启动你的应用。

swift
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            HomePage()
        }
    }
}

另一种常见的 SwiftUI 做法是将应用主体放置在符合 `View` 协议的 `struct` 中,如下所示:

swift
struct HomePage: View {
  var body: some View {
    Text("Hello, World!")
  }
}

要启动你的 **Flutter** 应用,将你的应用实例传递给 `runApp` 函数。

dart
void main() {
  runApp(const MyApp());
}

`App` 是一个组件。`build` 方法描述了它所代表的用户界面部分。通常,你的应用会以 `WidgetApp` 类开头,例如 `CupertinoApp`

dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // Returns a CupertinoApp that, by default,
    // has the look and feel of an iOS app.
    return const CupertinoApp(home: HomePage());
  }
}

`HomePage` 中使用的组件可能以 `Scaffold` 类开头。`Scaffold` 为应用实现了一个基本的布局结构。

dart
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(body: Center(child: Text('Hello, World!')));
  }
}

请注意 Flutter 如何使用 `Center` 组件。SwiftUI 默认将其视图内容渲染在其中心。Flutter 并非总是如此。`Scaffold` 不会将其 `body` 组件渲染在屏幕中心。要使文本居中,请将其包装在 `Center` 组件中。要了解不同的组件及其默认行为,请查阅组件目录

添加按钮

#

在 **SwiftUI** 中,你使用 `Button` 结构体来创建按钮。

swift
Button("Do something") {
  // this closure gets called when your
  // button is tapped
}

要在 **Flutter** 中实现相同的结果,请使用 `CupertinoButton` 类:

dart
CupertinoButton(
  onPressed: () {
    // This closure is called when your button is tapped.
  },
  const Text('Do something'),
),

**Flutter** 让你能够访问各种具有预定义样式的按钮。`CupertinoButton` 类来自 Cupertino 库。Cupertino 库中的组件使用 Apple 的设计系统。

水平对齐组件

#

在 **SwiftUI** 中,堆栈视图在设计布局方面扮演着重要角色。两个独立的结构允许你创建堆栈:

  1. `HStack` 用于水平堆栈视图

  2. `VStack` 用于垂直堆栈视图

以下 SwiftUI 视图将一个地球仪图像和文本添加到水平堆栈视图:

swift
HStack {
  Image(systemName: "globe")
  Text("Hello, world!")
}

**Flutter** 使用 `Row` 而不是 `HStack`。

dart
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [Icon(CupertinoIcons.globe), Text('Hello, world!')],
),

`Row` 组件在 `children` 参数中需要一个 `List`。`mainAxisAlignment` 属性告诉 Flutter 如何在额外空间中定位子组件。`MainAxisAlignment.center` 将子组件定位在主轴的中心。对于 `Row`,主轴是水平轴。

垂直对齐组件

#

以下示例基于上一节中的示例。

在 **SwiftUI** 中,你使用 `VStack` 将组件排列成垂直列。

swift
VStack {
  Image(systemName: "globe")
  Text("Hello, world!")
}

**Flutter** 使用与上一个示例相同的 Dart 代码,只是它用 `Column` 替换了 `Row`。

dart
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [Icon(CupertinoIcons.globe), Text('Hello, world!')],
),

显示列表视图

#

在 **SwiftUI** 中,你使用 `List` 基本组件来显示一系列项目。要显示一系列模型对象,请确保用户可以识别你的模型对象。要使对象可识别,请使用 `Identifiable` 协议。

swift
struct Person: Identifiable {
  var name: String
}

var persons = [
  Person(name: "Person 1"),
  Person(name: "Person 2"),
  Person(name: "Person 3"),
]

struct ListWithPersons: View {
  let persons: [Person]
  var body: some View {
    List {
      ForEach(persons) { person in
        Text(person.name)
      }
    }
  }
}

这类似于 **Flutter** 偏好构建其列表组件的方式。Flutter 不需要列表项是可识别的。你设置要显示的项目数量,然后为每个项目构建一个组件。

dart
class Person {
  String name;
  Person(this.name);
}

final List<Person> items = [
  Person('Person 1'),
  Person('Person 2'),
  Person('Person 3'),
];

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(title: Text(items[index].name));
        },
      ),
    );
  }
}

Flutter 对列表有一些注意事项:

  • `ListView` 组件有一个构建器方法。这与 SwiftUI 的 `List` 结构体中的 `ForEach` 类似。

  • `ListView` 的 `itemCount` 参数设置 `ListView` 显示的项目数量。

  • `itemBuilder` 有一个索引参数,该参数将在零和 `itemCount` 减一之间。

上一个示例为每个项目返回了一个 `ListTile` 组件。`ListTile` 组件包含 `height` 和 `font-size` 等属性。这些属性有助于构建列表。然而,Flutter 允许你返回几乎任何代表你数据的组件。

显示网格

#

在 **SwiftUI** 中构建非条件网格时,你使用 `Grid` 和 `GridRow`。

swift
Grid {
  GridRow {
    Text("Row 1")
    Image(systemName: "square.and.arrow.down")
    Image(systemName: "square.and.arrow.up")
  }
  GridRow {
    Text("Row 2")
    Image(systemName: "square.and.arrow.down")
    Image(systemName: "square.and.arrow.up")
  }
}

要在 **Flutter** 中显示网格,请使用 `GridView` 组件。此组件有各种构造函数。每个构造函数的目标相似,但使用不同的输入参数。以下示例使用 `.builder()` 初始化器:

dart
const widgets = [
  Text('Row 1'),
  Icon(CupertinoIcons.arrow_down_square),
  Icon(CupertinoIcons.arrow_up_square),
  Text('Row 2'),
  Icon(CupertinoIcons.arrow_down_square),
  Icon(CupertinoIcons.arrow_up_square),
];

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisExtent: 40,
        ),
        itemCount: widgets.length,
        itemBuilder: (context, index) => widgets[index],
      ),
    );
  }
}

`SliverGridDelegateWithFixedCrossAxisCount` 委托决定了网格用于布局其组件的各种参数。这包括 `crossAxisCount`,它决定了每行显示的项目数量。

SwiftUI 的 `Grid` 和 Flutter 的 `GridView` 的区别在于 `Grid` 需要 `GridRow`。`GridView` 使用委托来决定网格应如何布局其组件。

创建滚动视图

#

在 **SwiftUI** 中,你使用 `ScrollView` 创建自定义滚动组件。以下示例以可滚动的方式显示一系列 `PersonView` 实例。

swift
ScrollView {
  VStack(alignment: .leading) {
    ForEach(persons) { person in
      PersonView(person: person)
    }
  }
}

为了创建滚动视图,**Flutter** 使用 `SingleChildScrollView`。在以下示例中,函数 `mockPerson` 模拟 `Person` 类的实例以创建自定义的 `PersonView` 组件。

dart
SingleChildScrollView(
  child: Column(
    children: mockPersons
        .map((person) => PersonView(person: person))
        .toList(),
  ),
),

响应式和自适应设计

#

在 **SwiftUI** 中,你使用 `GeometryReader` 来创建相对视图大小。

例如,你可以:

  • 将 `geometry.size.width` 乘以某个因子以设置*宽度*。
  • 使用 `GeometryReader` 作为断点来改变应用的设计。

你还可以使用 `horizontalSizeClass` 查看大小类是否为 `.regular` 或 `.compact`。

要在 **Flutter** 中创建相对视图,你可以使用以下两种选项之一:

要了解更多信息,请查阅创建响应式和自适应应用

状态管理

#

在 **SwiftUI** 中,你使用 `@State` 属性包装器来表示 SwiftUI 视图的内部状态。

swift
struct ContentView: View {
  @State private var counter = 0;
  var body: some View {
    VStack{
      Button("+") { counter+=1 }
      Text(String(counter))
    }
  }}

**SwiftUI** 还包括用于更复杂状态管理的一些选项,例如 `ObservableObject` 协议。

**Flutter** 使用 `StatefulWidget` 管理本地状态。使用以下两个类实现有状态组件:

  • `StatefulWidget` 的子类
  • `State` 的子类

`State` 对象存储组件的状态。要更改组件的状态,请从 `State` 子类调用 `setState()` 以告诉框架重新绘制组件。

以下示例显示了计数器应用的一部分:

dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('$_counter'),
            TextButton(
              onPressed: () => setState(() {
                _counter++;
              }),
              child: const Text('+'),
            ),
          ],
        ),
      ),
    );
  }
}

要了解更多管理状态的方法,请查阅状态管理

动画

#

存在两种主要的 UI 动画类型:

  • 隐式动画:从当前值动画到新的目标值。
  • 显式动画:在被要求时进行动画。

隐式动画

#

SwiftUI 和 Flutter 对动画采取了类似的方法。在这两个框架中,你都指定了诸如 `duration` 和 `curve` 等参数。

在 **SwiftUI** 中,你使用 `animate()` 修饰符来处理隐式动画。

swift
Button("Tap me!"){
   angle += 45
}
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 1))

**Flutter** 包含用于隐式动画的组件。这简化了常见组件的动画。Flutter 以 `AnimatedFoo` 的格式命名这些组件。

例如:要旋转按钮,请使用 `AnimatedRotation` 类。它会对 `Transform.rotate` 组件进行动画处理。

dart
AnimatedRotation(
  duration: const Duration(seconds: 1),
  turns: turns,
  curve: Curves.easeIn,
  TextButton(
    onPressed: () {
      setState(() {
        turns += .125;
      });
    },
    const Text('Tap me!'),
  ),
),

Flutter 允许你创建自定义隐式动画。要组合新的动画组件,请使用 `TweenAnimationBuilder`

显式动画

#

对于显式动画,**SwiftUI** 使用 `withAnimation()` 函数。

**Flutter** 包含格式为 `FooTransition` 的显式动画组件。一个例子是 `RotationTransition` 类。

Flutter 还允许你使用 `AnimatedWidget` 或 `AnimatedBuilder` 创建自定义显式动画。

要了解 Flutter 中的动画,请参阅动画概述

屏幕绘制

#

在 **SwiftUI** 中,你使用 `CoreGraphics` 在屏幕上绘制线条和形状。

**Flutter** 有一个基于 `Canvas` 类的 API,其中包含两个帮助你绘制的类:

  1. `CustomPaint`,它需要一个画家

    dart
    CustomPaint(
      painter: SignaturePainter(_points),
      size: Size.infinite,
    ),
  2. `CustomPainter`,它实现了你的算法以在画布上绘制。

    dart
    class SignaturePainter extends CustomPainter {
      SignaturePainter(this.points);
    
      final List<Offset?> points;
    
      @override
      void paint(Canvas canvas, Size size) {
        final Paint paint = Paint()
          ..color = Colors.black
          ..strokeCap = StrokeCap.round
          ..strokeWidth = 5;
        for (int i = 0; i < points.length - 1; i++) {
          if (points[i] != null && points[i + 1] != null) {
            canvas.drawLine(points[i]!, points[i + 1]!, paint);
          }
        }
      }
    
      @override
      bool shouldRepaint(SignaturePainter oldDelegate) =>
          oldDelegate.points != points;
    }
#

本节解释了如何在应用页面之间导航、推入和弹出机制等等。

#

开发者构建的 iOS 和 macOS 应用具有不同页面,称为*导航路由*。

在 **SwiftUI** 中,`NavigationStack` 代表这个页面堆栈。

以下示例创建了一个显示人员列表的应用。要在新导航链接中显示人员详细信息,请点击该人员。

swift
NavigationStack(path: $path) {
      List {
        ForEach(persons) { person in
          NavigationLink(
            person.name,
            value: person
          )
        }
      }
      .navigationDestination(for: Person.self) { person in
        PersonView(person: person)
      }
    }

如果你的 **Flutter** 应用很小,没有复杂的链接,可以使用带命名路由的 `Navigator`。定义导航路由后,可以使用它们的名称调用导航路由。

  1. 在传递给 `runApp()` 函数的类中命名每个路由。以下示例使用 `App`:

    dart
    // Defines the route name as a constant
    // so that it's reusable.
    const detailsPageRouteName = '/details';
    
    class App extends StatelessWidget {
      const App({super.key});
    
      @override
      Widget build(BuildContext context) {
        return CupertinoApp(
          home: const HomePage(),
          // The [routes] property defines the available named routes
          // and the widgets to build when navigating to those routes.
          routes: {detailsPageRouteName: (context) => const DetailsPage()},
        );
      }
    }

    以下示例使用 `mockPersons()` 生成人员列表。点击一个人会使用 `pushNamed()` 将该人员的详细信息页面推送到 `Navigator`。

    dart
    ListView.builder(
      itemCount: mockPersons.length,
      itemBuilder: (context, index) {
        final person = mockPersons.elementAt(index);
        final age = '${person.age} years old';
        return ListTile(
          title: Text(person.name),
          subtitle: Text(age),
          trailing: const Icon(Icons.arrow_forward_ios),
          onTap: () {
            // When a [ListTile] that represents a person is
            // tapped, push the detailsPageRouteName route
            // to the Navigator and pass the person's instance
            // to the route.
            Navigator.of(
              context,
            ).pushNamed(detailsPageRouteName, arguments: person);
          },
        );
      },
    ),
  2. 定义 `DetailsPage` 组件,它显示每个人的详细信息。在 Flutter 中,你可以在导航到新路由时将参数传递到组件中。使用 `ModalRoute.of()` 提取参数:

    dart
    class DetailsPage extends StatelessWidget {
      const DetailsPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        // Read the person instance from the arguments.
        final Person person = ModalRoute.of(context)?.settings.arguments as Person;
        // Extract the age.
        final age = '${person.age} years old';
        return Scaffold(
          // Display name and age.
          body: Column(children: [Text(person.name), Text(age)]),
        );
      }
    }

要创建更高级的导航和路由需求,请使用路由包,例如 go_router

要了解更多信息,请查阅导航和路由

手动返回

#

在 **SwiftUI** 中,你使用 `dismiss` 环境值来弹回上一屏幕。

swift
Button("Pop back") {
        dismiss()
      }

在 **Flutter** 中,使用 `Navigator` 类的 `pop()` 函数:

dart
TextButton(
  onPressed: () {
    // This code allows the
    // view to pop back to its presenter.
    Navigator.of(context).pop();
  },
  child: const Text('Pop back'),
),
#

在 **SwiftUI** 中,你使用 `openURL` 环境变量来打开指向另一个应用的 URL。

swift
@Environment(\.openURL) private var openUrl

// View code goes here

 Button("Open website") {
      openUrl(
        URL(
          string: "https://google.com"
        )!
      )
    }

在 **Flutter** 中,使用 `url_launcher` 插件。

dart
CupertinoButton(
  onPressed: () async {
    await launchUrl(Uri.parse('https://google.com'));
  },
  const Text('Open website'),
),

主题、样式和媒体

#

你可以轻松地为 Flutter 应用设置样式。样式包括在明亮和暗黑主题之间切换、更改文本和 UI 组件的设计等等。本节介绍如何为你的应用设置样式。

使用暗黑模式

#

在 **SwiftUI** 中,你对 `View` 调用 `preferredColorScheme()` 函数以使用暗黑模式。

在 **Flutter** 中,你可以在应用级别控制亮色和暗黑模式。要控制亮度模式,请使用 `App` 类的 `theme` 属性:

dart
const CupertinoApp(
  theme: CupertinoThemeData(brightness: Brightness.dark),
  home: HomePage(),
);

文本样式

#

在 **SwiftUI** 中,你使用修饰符函数来设置文本样式。例如,要更改 `Text` 字符串的字体,请使用 `font()` 修饰符:

swift
Text("Hello, world!")
  .font(.system(size: 30, weight: .heavy))
  .foregroundColor(.yellow)

要在 **Flutter** 中设置文本样式,请添加 `TextStyle` 组件作为 `Text` 组件的 `style` 参数的值。

dart
Text(
  'Hello, world!',
  style: TextStyle(
    fontSize: 30,
    fontWeight: FontWeight.bold,
    color: CupertinoColors.systemYellow,
  ),
),

按钮样式

#

在 **SwiftUI** 中,你使用修饰符函数来设置按钮样式。

swift
Button("Do something") {
    // do something when button is tapped
  }
  .font(.system(size: 30, weight: .bold))
  .background(Color.yellow)
  .foregroundColor(Color.blue)
}

要在 **Flutter** 中设置按钮组件的样式,请设置其子项的样式,或修改按钮本身的属性。

在以下示例中:

  • `CupertinoButton` 的 `color` 属性设置其*颜色*。
  • 子 `Text` 组件的 `color` 属性设置按钮文本颜色。
dart
child: CupertinoButton(
  color: CupertinoColors.systemYellow,
  onPressed: () {},
  padding: const EdgeInsets.all(16),
  child: const Text(
    'Do something',
    style: TextStyle(
      color: CupertinoColors.systemBlue,
      fontSize: 30,
      fontWeight: FontWeight.bold,
    ),
  ),
),

使用自定义字体

#

在 **SwiftUI** 中,你可以通过两个步骤在应用中使用自定义字体。首先,将字体文件添加到你的 SwiftUI 项目。添加文件后,使用 `.font()` 修饰符将其应用于你的 UI 组件。

swift
Text("Hello")
  .font(
    Font.custom(
      "BungeeSpice-Regular",
      size: 40
    )
  )

在 **Flutter** 中,你使用名为 `pubspec.yaml` 的文件控制资源。此文件与平台无关。要将自定义字体添加到你的项目,请按照以下步骤操作:

  1. 在项目根目录中创建一个名为 `fonts` 的文件夹。此可选步骤有助于组织你的字体。

  2. 将你的 `.ttf`、`.otf` 或 `.ttc` 字体文件添加到 `fonts` 文件夹中。

  3. 打开项目中的 `pubspec.yaml` 文件。

  4. 找到 `flutter` 部分。

  5. 在 `fonts` 部分下添加你的自定义字体。

    yaml
    flutter:
      fonts:
        - family: BungeeSpice
          fonts:
            - asset: fonts/BungeeSpice-Regular.ttf

将字体添加到项目后,你可以按照以下示例使用它:

dart
Text(
  'Cupertino',
  style: TextStyle(fontSize: 40, fontFamily: 'BungeeSpice'),
),

在应用中捆绑图片

#

在 **SwiftUI** 中,你首先将图像文件添加到 `Assets.xcassets`,然后使用 `Image` 视图显示图像。

要在 **Flutter** 中添加图片,请遵循与添加自定义字体类似的方法。

  1. 在根目录中添加一个 `images` 文件夹。

  2. 将此资源添加到 `pubspec.yaml` 文件中。

    yaml
    flutter:
      assets:
        - images/Blueberries.jpg

添加图像后,使用 `Image` 组件的 `.asset()` 构造函数显示它。此构造函数:

  1. 使用提供的路径实例化给定图像。
  2. 从与你的应用捆绑的资源中读取图像。
  3. 在屏幕上显示图像。

要查看完整示例,请查阅 `Image` 文档。

在应用中捆绑视频

#

在 **SwiftUI** 中,你可以通过两个步骤将本地视频文件捆绑到你的应用中。首先,导入 `AVKit` 框架,然后实例化一个 `VideoPlayer` 视图。

在 **Flutter** 中,将 video_player 插件添加到你的项目。此插件允许你从同一个代码库创建可在 Android、iOS 和 Web 上运行的视频播放器。

  1. 将插件添加到你的应用,并将视频文件添加到你的项目。
  2. 将资源添加到你的 `pubspec.yaml` 文件中。
  3. 使用 `VideoPlayerController` 类加载和播放你的视频文件。

要查看完整的操作指南,请查阅 video_player 示例