Flutter iOS 风格弹框组件封装

📱 概述

在 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);
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

🎨 设计特点

  1. 视觉一致性

所有组件都遵循 iOS 设计规范:

· 使用 Cupertino 组件库

· 圆角设计

· 系统配色

· 原生动画效果

  1. 用户体验优化

· 符合 iOS 操作习惯

· 流畅的过渡动画

· 合理的交互反馈

· 无障碍支持

  1. 开发者友好

· 类型安全的 API

· 完善的参数说明

· 清晰的错误提示

· 易于扩展和维护


📝 最佳实践

  1. 弹框层级管理
dart 复制代码
// 确保不会同时显示多个弹框
void showDialogSafely(BuildContext context) async {
  if (ModalRoute.of(context)?.isCurrent == true) {
    await IosDialog.showAlert(...);
  }
}
  1. 错误处理
dart 复制代码
void safeShowDialog(BuildContext context) {
  try {
    IosDialog.showAlert(...);
  } catch (e) {
    debugPrint('弹框显示失败: $e');
  }
}
  1. 内存管理
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),
          ),
        ],
      ),
    );
  }
}

📊 性能考虑

  1. 避免频繁弹框:控制弹框显示频率
  2. 及时释放资源:弹框关闭时清理资源
  3. 使用异步操作:避免阻塞 UI 线程
  4. 懒加载内容:复杂弹框内容延迟加载

🎯 总结

这个 iOS 弹框封装库提供了:

优点

· ✅ 完整的弹框类型覆盖

· ✅ 原生 iOS 视觉体验

· ✅ 简洁易用的 API

· ✅ 良好的性能表现

· ✅ 易于扩展和维护

适用场景

· iOS 专用应用开发

· 需要 iOS 风格体验的跨平台应用

· 对 UI/UX 一致性要求高的项目

后续优化方向

  1. 增加更多动画效果选项
  2. 支持暗色模式适配
  3. 添加国际化支持
  4. 提供更多自定义主题选项

通过这个封装,你可以快速为 Flutter 应用添加专业的 iOS 风格弹框,提升用户体验和开发效率。


相关推荐
程序员Ctrl喵15 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难16 小时前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡17 小时前
flutter列表中实现置顶动画
flutter
始持18 小时前
第十二讲 风格与主题统一
前端·flutter
始持18 小时前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持18 小时前
第十三讲 异步操作与异步构建
前端·flutter
新镜18 小时前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴19 小时前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区19 小时前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎20 小时前
树形选择器组件封装
前端·flutter