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 风格弹框,提升用户体验和开发效率。


相关推荐
末代iOS程序员华仔2 小时前
Flutter 开发 iOS App 上架全流程与常见问题分析
flutter·ios
末代iOS程序员华仔2 小时前
Flutter开发iOS海外工具类应用上架合规指南与风险规避
flutter·ios
蜡台14 小时前
Flutter 环境搭建 Dart配置
flutter·dart
落叶飘飘s1 天前
餐饮服务与软件创新的融合:解析海底捞 APP 的 Flutter 鸿蒙开发之路
flutter·华为·harmonyos
起司喵喵1 天前
Flutter-MacOS桌面OS系统|flutter.+window_manager客户端OS模板
flutter·macos·策略模式
Wuxiaoming1351 天前
flutter app的logo和splash logo尺寸
flutter
GitLqr2 天前
深入理解 Flutter 架构:从 Widget 到 GPU 像素的全链路解析
flutter·面试·dart
SoaringHeart2 天前
Flutter进阶|最佳实践:组件内阴影实现
前端·flutter
玛艾露贝2 天前
Supabase云同步架构:Flutter应用的数据同步策略
flutter·架构
魔力女仆2 天前
Flutter 复杂拖拽排序实战:同源排序 + 跨容器拖拽完整落地
flutter