📱 概述
在 Flutter 开发中,为了给 iOS 用户提供原生般的体验,使用 Cupertino 风格的弹框至关重要。本文将介绍如何封装一套完整、易用的 iOS 风格弹框组件库,涵盖日常开发中所需的所有弹框类型。
📁 项目结构
lib/
├── ios_dialog.dart # iOS 弹框主类
├── ios_dialog_example.dart # 使用示例
└── widgets/ # 相关组件
🔧 核心实现
ios_dialog.dart
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// iOS 风格弹框工具类
/// 提供完整的 iOS 原生体验弹框组件
class IosDialog {
// ================================
// 1. 警告弹框 (Alert)
// ================================
/// 基础警告弹框
///
/// 参数说明:
/// - title: 标题(必需)
/// - message: 消息内容(可选)
/// - actions: 自定义按钮(可选,默认提供确定按钮)
static Future<T?> showAlert<T>({
required BuildContext context,
required String title,
String? message,
List<CupertinoDialogAction>? actions,
bool barrierDismissible = true,
}) {
return showCupertinoDialog<T>(
context: context,
barrierDismissible: barrierDismissible,
builder: (context) => CupertinoAlertDialog(
title: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 17,
),
),
content: message != null ? Text(message) : null,
actions: actions ?? [
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('确定'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
/// 确认弹框
///
/// 用于需要用户确认的操作,返回 true/false
static Future<bool?> showConfirm({
required BuildContext context,
required String title,
String? message,
String confirmText = '确定',
String cancelText = '取消',
bool destructive = false,
}) {
return showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(title),
content: message != null ? Text(message) : null,
actions: [
CupertinoDialogAction(
child: Text(cancelText),
onPressed: () => Navigator.of(context).pop(false),
),
CupertinoDialogAction(
isDefaultAction: true,
isDestructiveAction: destructive,
child: Text(confirmText),
onPressed: () => Navigator.of(context).pop(true),
),
],
),
);
}
/// 多个选项的选择弹框
static Future<int?> showChoice({
required BuildContext context,
required String title,
String? message,
required List<String> options,
String cancelText = '取消',
}) {
return showCupertinoDialog<int>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(title),
content: message != null ? Text(message) : null,
actions: [
for (var i = 0; i < options.length; i++)
CupertinoDialogAction(
child: Text(options[i]),
onPressed: () => Navigator.of(context).pop(i),
),
CupertinoDialogAction(
child: Text(cancelText),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
// ================================
// 2. 底部操作菜单 (ActionSheet)
// ================================
/// 基础底部操作菜单
static Future<T?> showActionSheet<T>({
required BuildContext context,
String? title,
String? message,
String? cancelText,
required List<CupertinoActionSheetAction> actions,
bool showCancel = true,
}) {
return showCupertinoModalPopup<T>(
context: context,
semanticsDismissible: true,
builder: (context) => CupertinoActionSheet(
title: title != null
? Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
color: CupertinoColors.secondaryLabel,
),
)
: null,
message: message != null
? Text(
message,
style: const TextStyle(
fontSize: 13,
color: CupertinoColors.secondaryLabel,
),
)
: null,
actions: actions,
cancelButton: showCancel
? CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () => Navigator.of(context).pop(),
child: Text(
cancelText ?? '取消',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
)
: null,
),
);
}
/// 快捷创建操作菜单
static Future<void> showSimpleActionSheet({
required BuildContext context,
required String title,
required List<ActionSheetItem> items,
String? cancelText,
}) {
final actions = items.map((item) {
return CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop();
item.onPressed?.call();
},
child: Text(
item.title,
style: item.destructive == true
? const TextStyle(color: CupertinoColors.systemRed)
: null,
),
);
}).toList();
return showActionSheet(
context: context,
title: title,
actions: actions,
cancelText: cancelText,
);
}
// ================================
// 3. 选择器 (Picker)
// ================================
/// 列表选择器
static Future<T?> showPicker<T>({
required BuildContext context,
required List<T> items,
required String Function(T) itemBuilder,
T? initialItem,
String title = '请选择',
String confirmText = '完成',
String cancelText = '取消',
}) {
T? selectedItem = initialItem ?? items.first;
return showCupertinoModalPopup<T>(
context: context,
builder: (context) => Container(
height: 250,
color: CupertinoColors.systemBackground.resolveFrom(context),
child: Column(
children: [
// 顶部工具栏
_buildPickerToolbar(
context: context,
title: title,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: () => Navigator.of(context).pop(selectedItem),
onCancel: () => Navigator.of(context).pop(),
),
// 选择器主体
Expanded(
child: CupertinoPicker(
scrollController: FixedExtentScrollController(
initialItem: items.indexOf(selectedItem!),
),
itemExtent: 40,
onSelectedItemChanged: (index) {
selectedItem = items[index];
},
children: items.map((item) {
return Center(
child: Text(
itemBuilder(item),
style: const TextStyle(fontSize: 20),
),
);
}).toList(),
),
),
],
),
),
);
}
// ================================
// 4. 输入弹框
// ================================
/// 文本输入弹框
static Future<String?> showInput({
required BuildContext context,
required String title,
String? placeholder,
String? initialValue,
String confirmText = '确定',
String cancelText = '取消',
int? maxLength,
bool obscureText = false,
}) {
final controller = TextEditingController(text: initialValue);
return showCupertinoDialog<String>(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return CupertinoAlertDialog(
title: Text(title),
content: SizedBox(
height: 80,
child: CupertinoTextField(
controller: controller,
placeholder: placeholder,
maxLength: maxLength,
obscureText: obscureText,
autofocus: true,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
border: Border.all(
color: CupertinoColors.quaternaryLabel,
),
borderRadius: BorderRadius.circular(8),
),
),
),
actions: [
CupertinoDialogAction(
child: Text(cancelText),
onPressed: () => Navigator.of(context).pop(),
),
CupertinoDialogAction(
isDefaultAction: true,
child: Text(confirmText),
onPressed: () {
if (controller.text.isNotEmpty) {
Navigator.of(context).pop(controller.text);
}
},
),
],
);
},
);
},
);
}
// ================================
// 5. 加载弹框
// ================================
/// 显示加载提示
static void showLoading({
required BuildContext context,
String message = '加载中...',
}) {
showCupertinoDialog(
context: context,
barrierDismissible: false,
builder: (context) => CupertinoAlertDialog(
content: SizedBox(
height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CupertinoActivityIndicator(radius: 16),
const SizedBox(height: 16),
Text(
message,
style: TextStyle(
fontSize: 14,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
],
),
),
),
);
}
/// 隐藏加载弹框
static void hideLoading(BuildContext context) {
Navigator.of(context).pop();
}
// ================================
// 6. Toast 提示
// ================================
/// 显示 Toast 提示
static void showToast({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 2),
ToastPosition position = ToastPosition.center,
}) {
final overlay = Overlay.of(context);
final overlayEntry = OverlayEntry(
builder: (context) => _buildToastWidget(
context: context,
message: message,
position: position,
),
);
overlay.insert(overlayEntry);
Future.delayed(duration, overlayEntry.remove);
}
// ================================
// 工具方法
// ================================
/// 构建选择器顶部工具栏
static Widget _buildPickerToolbar({
required BuildContext context,
required String title,
required String confirmText,
required String cancelText,
required VoidCallback onConfirm,
required VoidCallback onCancel,
}) {
return Container(
height: 44,
decoration: BoxDecoration(
color: CupertinoColors.secondarySystemBackground.resolveFrom(context),
border: Border(
bottom: BorderSide(
color: CupertinoColors.separator.resolveFrom(context),
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 16),
onPressed: onCancel,
child: Text(cancelText),
),
Text(
title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 16),
onPressed: onConfirm,
child: Text(
confirmText,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
);
}
/// 构建 Toast 组件
static Widget _buildToastWidget({
required BuildContext context,
required String message,
required ToastPosition position,
}) {
double? top, bottom;
switch (position) {
case ToastPosition.top:
top = MediaQuery.of(context).padding.top + 50;
break;
case ToastPosition.center:
top = MediaQuery.of(context).size.height / 2 - 25;
break;
case ToastPosition.bottom:
bottom = MediaQuery.of(context).padding.bottom + 100;
break;
}
return Positioned(
top: top,
bottom: bottom,
left: 0,
right: 0,
child: Material(
color: Colors.transparent,
child: Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: CupertinoColors.systemFill.withOpacity(0.9),
borderRadius: BorderRadius.circular(10),
),
child: Text(
message,
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 14,
),
),
),
),
),
);
}
}
// ================================
// 数据结构
// ================================
/// ActionSheet 项目
class ActionSheetItem {
final String title;
final VoidCallback? onPressed;
final bool destructive;
ActionSheetItem({
required this.title,
this.onPressed,
this.destructive = false,
});
}
/// Toast 位置
enum ToastPosition {
top,
center,
bottom,
}
💡 使用示例
基本使用方式
dart
import 'ios_dialog.dart';
class ExamplePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('弹框示例'),
),
child: SafeArea(
child: ListView(
children: [
// 1. 警告弹框
CupertinoButton(
child: const Text('基本警告'),
onPressed: () => IosDialog.showAlert(
context: context,
title: '提示',
message: '操作成功',
),
),
// 2. 确认弹框
CupertinoButton(
child: const Text('确认弹框'),
onPressed: () async {
final result = await IosDialog.showConfirm(
context: context,
title: '确认删除',
message: '确定要删除吗?',
destructive: true,
);
if (result == true) {
IosDialog.showToast(
context: context,
message: '已删除',
);
}
},
),
// 3. 底部菜单
CupertinoButton(
child: const Text('操作菜单'),
onPressed: () {
IosDialog.showSimpleActionSheet(
context: context,
title: '选择操作',
items: [
ActionSheetItem(
title: '编辑',
onPressed: () => print('编辑'),
),
ActionSheetItem(
title: '删除',
destructive: true,
onPressed: () => print('删除'),
),
],
);
},
),
// 4. 选择器
CupertinoButton(
child: const Text('选择器'),
onPressed: () async {
final result = await IosDialog.showPicker(
context: context,
items: ['选项一', '选项二', '选项三'],
itemBuilder: (item) => item,
);
if (result != null) {
print('选择了: $result');
}
},
),
// 5. 输入弹框
CupertinoButton(
child: const Text('输入弹框'),
onPressed: () async {
final text = await IosDialog.showInput(
context: context,
title: '请输入',
placeholder: '输入内容...',
);
if (text != null) {
print('输入了: $text');
}
},
),
// 6. 加载弹框
CupertinoButton(
child: const Text('加载弹框'),
onPressed: () {
IosDialog.showLoading(context: context);
Future.delayed(const Duration(seconds: 2), () {
IosDialog.hideLoading(context);
});
},
),
],
),
),
);
}
}
🎨 设计特点
- 视觉一致性
所有组件都遵循 iOS 设计规范:
· 使用 Cupertino 组件库
· 圆角设计
· 系统配色
· 原生动画效果
- 用户体验优化
· 符合 iOS 操作习惯
· 流畅的过渡动画
· 合理的交互反馈
· 无障碍支持
- 开发者友好
· 类型安全的 API
· 完善的参数说明
· 清晰的错误提示
· 易于扩展和维护
📝 最佳实践
- 弹框层级管理
dart
// 确保不会同时显示多个弹框
void showDialogSafely(BuildContext context) async {
if (ModalRoute.of(context)?.isCurrent == true) {
await IosDialog.showAlert(...);
}
}
- 错误处理
dart
void safeShowDialog(BuildContext context) {
try {
IosDialog.showAlert(...);
} catch (e) {
debugPrint('弹框显示失败: $e');
}
}
- 内存管理
dart
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
void dispose() {
// 页面销毁时关闭所有弹框
Navigator.of(context).popUntil((route) => route.isFirst);
super.dispose();
}
}
🔄 进阶用法
自定义扩展
dart
/// 自定义带图标的弹框
extension IosDialogWithIcon on IosDialog {
static Future<void> showAlertWithIcon({
required BuildContext context,
required String title,
required IconData icon,
Color iconColor = CupertinoColors.activeBlue,
}) {
return showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Row(
children: [
Icon(icon, color: iconColor),
const SizedBox(width: 8),
Text(title),
],
),
actions: [
CupertinoDialogAction(
child: const Text('确定'),
onPressed: () => Navigator.pop(context),
),
],
),
);
}
}
📊 性能考虑
- 避免频繁弹框:控制弹框显示频率
- 及时释放资源:弹框关闭时清理资源
- 使用异步操作:避免阻塞 UI 线程
- 懒加载内容:复杂弹框内容延迟加载
🎯 总结
这个 iOS 弹框封装库提供了:
优点
· ✅ 完整的弹框类型覆盖
· ✅ 原生 iOS 视觉体验
· ✅ 简洁易用的 API
· ✅ 良好的性能表现
· ✅ 易于扩展和维护
适用场景
· iOS 专用应用开发
· 需要 iOS 风格体验的跨平台应用
· 对 UI/UX 一致性要求高的项目
后续优化方向
- 增加更多动画效果选项
- 支持暗色模式适配
- 添加国际化支持
- 提供更多自定义主题选项
通过这个封装,你可以快速为 Flutter 应用添加专业的 iOS 风格弹框,提升用户体验和开发效率。