Flutter Widgets Fundamentals

Cupertino (iOS-style) Widgets

45 min Lesson 13 of 18

iOS-style Widgets in Flutter

Flutter includes a complete set of iOS-style widgets under the cupertino library. These widgets replicate the native look and feel of iOS apps, making your Flutter app feel at home on Apple devices. You can even mix Material and Cupertino widgets in the same app to create platform-adaptive interfaces.

Note: To use Cupertino widgets, import package:flutter/cupertino.dart. You can import both material.dart and cupertino.dart in the same file without conflicts.

CupertinoApp & CupertinoPageScaffold

CupertinoApp is the iOS equivalent of MaterialApp. It sets up Cupertino theming. CupertinoPageScaffold provides the basic page structure with a navigation bar and content area.

Basic Cupertino App

import 'package:flutter/cupertino.dart';

void main() => runApp(const MyCupertinoApp());

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

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'iOS Style App',
      theme: CupertinoThemeData(
        primaryColor: CupertinoColors.activeBlue,
        brightness: Brightness.light,
      ),
      home: HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Home'),
      ),
      child: Center(
        child: Text('Hello, iOS!'),
      ),
    );
  }
}

CupertinoNavigationBar

The iOS-style navigation bar sits at the top with a centred title and optional leading/trailing widgets.

Navigation Bar with Actions

CupertinoPageScaffold(
  navigationBar: CupertinoNavigationBar(
    middle: const Text('Settings'),
    leading: CupertinoButton(
      padding: EdgeInsets.zero,
      onPressed: () => Navigator.pop(context),
      child: const Icon(CupertinoIcons.back),
    ),
    trailing: CupertinoButton(
      padding: EdgeInsets.zero,
      onPressed: () {
        debugPrint('Edit tapped');
      },
      child: const Text('Edit'),
    ),
  ),
  child: const SafeArea(
    child: Center(child: Text('Content here')),
  ),
)

CupertinoButton

The iOS-style button with a press-down opacity effect instead of the Material ripple.

Cupertino Buttons

Column(
  children: [
    // Default (text) button
    CupertinoButton(
      onPressed: () => debugPrint('Pressed'),
      child: const Text('Text Button'),
    ),

    // Filled button
    CupertinoButton.filled(
      onPressed: () => debugPrint('Filled pressed'),
      child: const Text('Filled Button'),
    ),

    // Disabled button
    const CupertinoButton(
      onPressed: null,
      child: Text('Disabled'),
    ),

    // Custom styled button
    CupertinoButton(
      color: CupertinoColors.destructiveRed,
      borderRadius: BorderRadius.circular(8),
      onPressed: () => debugPrint('Delete'),
      child: const Text('Delete Account'),
    ),
  ],
)

CupertinoTextField

The iOS-style text field with rounded borders and placeholder text.

CupertinoTextField Examples

Column(
  children: [
    // Basic text field
    const CupertinoTextField(
      placeholder: 'Enter your name',
      padding: EdgeInsets.all(12),
    ),

    const SizedBox(height: 16),

    // Styled text field with prefix
    CupertinoTextField(
      placeholder: 'Search...',
      prefix: const Padding(
        padding: EdgeInsets.only(left: 8),
        child: Icon(
          CupertinoIcons.search,
          color: CupertinoColors.systemGrey,
        ),
      ),
      decoration: BoxDecoration(
        color: CupertinoColors.systemGrey6,
        borderRadius: BorderRadius.circular(10),
      ),
      padding: const EdgeInsets.all(12),
      clearButtonMode: OverlayVisibilityMode.editing,
    ),

    const SizedBox(height: 16),

    // Password field
    const CupertinoTextField(
      placeholder: 'Password',
      obscureText: true,
      padding: EdgeInsets.all(12),
      suffix: Padding(
        padding: EdgeInsets.only(right: 8),
        child: Icon(
          CupertinoIcons.eye_slash,
          color: CupertinoColors.systemGrey,
        ),
      ),
    ),
  ],
)

CupertinoSwitch & CupertinoSlider

iOS-style toggle switches and sliders.

Switch and Slider

class ControlsExample extends StatefulWidget {
  const ControlsExample({super.key});

  @override
  State<ControlsExample> createState() => _ControlsExampleState();
}

class _ControlsExampleState extends State<ControlsExample> {
  bool _wifiEnabled = true;
  double _brightness = 0.7;

  @override
  Widget build(BuildContext context) {
    return CupertinoListSection.insetGrouped(
      header: const Text('Display & Network'),
      children: [
        CupertinoListTile(
          title: const Text('Wi-Fi'),
          trailing: CupertinoSwitch(
            value: _wifiEnabled,
            onChanged: (bool value) {
              setState(() => _wifiEnabled = value);
            },
          ),
        ),
        CupertinoListTile(
          title: const Text('Brightness'),
          trailing: SizedBox(
            width: 180,
            child: CupertinoSlider(
              value: _brightness,
              onChanged: (double value) {
                setState(() => _brightness = value);
              },
            ),
          ),
        ),
      ],
    );
  }
}
Tip: Use CupertinoListSection.insetGrouped to create the grouped settings-style layout common in iOS apps. It automatically handles the rounded corners and section headers.

CupertinoAlertDialog

The iOS-style alert dialog with rounded corners and stacked or side-by-side action buttons.

Cupertino Alert Dialog

void _showCupertinoAlert(BuildContext context) {
  showCupertinoDialog(
    context: context,
    builder: (BuildContext ctx) {
      return CupertinoAlertDialog(
        title: const Text('Delete Photo'),
        content: const Text(
          'This photo will be deleted from all your devices. '
          'You can recover it from Recently Deleted for 30 days.',
        ),
        actions: [
          CupertinoDialogAction(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('Cancel'),
          ),
          CupertinoDialogAction(
            isDestructiveAction: true,
            onPressed: () {
              Navigator.pop(ctx);
              debugPrint('Photo deleted');
            },
            child: const Text('Delete'),
          ),
        ],
      );
    },
  );
}

CupertinoActionSheet

An action sheet slides up from the bottom and presents a set of options. It always includes a cancel button.

Cupertino Action Sheet

void _showActionSheet(BuildContext context) {
  showCupertinoModalPopup(
    context: context,
    builder: (BuildContext ctx) {
      return CupertinoActionSheet(
        title: const Text('Share Photo'),
        message: const Text('Choose how you want to share this photo.'),
        actions: [
          CupertinoActionSheetAction(
            onPressed: () {
              Navigator.pop(ctx);
              debugPrint('AirDrop');
            },
            child: const Text('AirDrop'),
          ),
          CupertinoActionSheetAction(
            onPressed: () {
              Navigator.pop(ctx);
              debugPrint('Messages');
            },
            child: const Text('Messages'),
          ),
          CupertinoActionSheetAction(
            isDestructiveAction: true,
            onPressed: () {
              Navigator.pop(ctx);
              debugPrint('Delete');
            },
            child: const Text('Delete Photo'),
          ),
        ],
        cancelButton: CupertinoActionSheetAction(
          isDefaultAction: true,
          onPressed: () => Navigator.pop(ctx),
          child: const Text('Cancel'),
        ),
      );
    },
  );
}

CupertinoPicker & CupertinoDatePicker

iOS-style scroll wheel pickers for selecting values and dates.

Cupertino Picker

void _showPicker(BuildContext context) {
  final List<String> countries = [
    'Saudi Arabia', 'UAE', 'Egypt',
    'Jordan', 'Kuwait', 'Qatar',
  ];

  showCupertinoModalPopup(
    context: context,
    builder: (ctx) {
      return Container(
        height: 250,
        color: CupertinoColors.systemBackground,
        child: CupertinoPicker(
          itemExtent: 36,
          onSelectedItemChanged: (int index) {
            debugPrint('Selected: \${countries[index]}');
          },
          children: countries.map((c) => Center(child: Text(c))).toList(),
        ),
      );
    },
  );
}

Cupertino Date Picker

void _showDatePicker(BuildContext context) {
  showCupertinoModalPopup(
    context: context,
    builder: (ctx) {
      return Container(
        height: 300,
        color: CupertinoColors.systemBackground,
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CupertinoButton(
                  child: const Text('Cancel'),
                  onPressed: () => Navigator.pop(ctx),
                ),
                CupertinoButton(
                  child: const Text('Done'),
                  onPressed: () => Navigator.pop(ctx),
                ),
              ],
            ),
            Expanded(
              child: CupertinoDatePicker(
                mode: CupertinoDatePickerMode.date,
                initialDateTime: DateTime.now(),
                minimumDate: DateTime(2000),
                maximumDate: DateTime(2030),
                onDateTimeChanged: (DateTime date) {
                  debugPrint('Date: \$date');
                },
              ),
            ),
          ],
        ),
      );
    },
  );
}

Platform-Adaptive Widgets

You can build widgets that automatically switch between Material and Cupertino styles based on the platform.

Adaptive Dialog

import 'dart:io' show Platform;

void showAdaptiveAlert(BuildContext context) {
  if (Platform.isIOS || Platform.isMacOS) {
    showCupertinoDialog(
      context: context,
      builder: (ctx) => CupertinoAlertDialog(
        title: const Text('Alert'),
        content: const Text('Something happened.'),
        actions: [
          CupertinoDialogAction(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  } else {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Alert'),
        content: const Text('Something happened.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}

Adaptive Switch (Built-in)

// Flutter 3.x provides Switch.adaptive and others
Switch.adaptive(
  value: _isEnabled,
  onChanged: (bool value) {
    setState(() => _isEnabled = value);
  },
)

// Also available:
// Slider.adaptive(...)
// CircularProgressIndicator.adaptive()
Warning: The dart:io library is not available on Flutter Web. If your app targets web, use Theme.of(context).platform or defaultTargetPlatform from foundation.dart instead of Platform.isIOS.

Practical Example: iOS Settings Clone

A realistic iOS Settings page built entirely with Cupertino widgets.

iOS Settings Screen

class SettingsScreen extends StatefulWidget {
  const SettingsScreen({super.key});

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  bool _airplaneMode = false;
  bool _wifi = true;
  bool _bluetooth = true;
  bool _notifications = true;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Settings'),
      ),
      child: SafeArea(
        child: ListView(
          children: [
            CupertinoListSection.insetGrouped(
              children: [
                CupertinoListTile(
                  leading: Container(
                    padding: const EdgeInsets.all(4),
                    decoration: BoxDecoration(
                      color: CupertinoColors.systemOrange,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: const Icon(
                      CupertinoIcons.airplane,
                      color: CupertinoColors.white,
                      size: 20,
                    ),
                  ),
                  title: const Text('Airplane Mode'),
                  trailing: CupertinoSwitch(
                    value: _airplaneMode,
                    onChanged: (v) => setState(() => _airplaneMode = v),
                  ),
                ),
                CupertinoListTile(
                  leading: Container(
                    padding: const EdgeInsets.all(4),
                    decoration: BoxDecoration(
                      color: CupertinoColors.activeBlue,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: const Icon(
                      CupertinoIcons.wifi,
                      color: CupertinoColors.white,
                      size: 20,
                    ),
                  ),
                  title: const Text('Wi-Fi'),
                  additionalInfo: Text(_wifi ? 'Home Network' : 'Off'),
                  trailing: const CupertinoListTileChevron(),
                ),
                CupertinoListTile(
                  leading: Container(
                    padding: const EdgeInsets.all(4),
                    decoration: BoxDecoration(
                      color: CupertinoColors.activeBlue,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: const Icon(
                      CupertinoIcons.bluetooth,
                      color: CupertinoColors.white,
                      size: 20,
                    ),
                  ),
                  title: const Text('Bluetooth'),
                  additionalInfo: Text(_bluetooth ? 'On' : 'Off'),
                  trailing: const CupertinoListTileChevron(),
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              children: [
                CupertinoListTile(
                  leading: Container(
                    padding: const EdgeInsets.all(4),
                    decoration: BoxDecoration(
                      color: CupertinoColors.systemRed,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: const Icon(
                      CupertinoIcons.bell_fill,
                      color: CupertinoColors.white,
                      size: 20,
                    ),
                  ),
                  title: const Text('Notifications'),
                  trailing: const CupertinoListTileChevron(),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Summary

  • CupertinoApp + CupertinoPageScaffold -- iOS app structure and page layout
  • CupertinoNavigationBar -- iOS-style top bar with centred title
  • CupertinoButton -- opacity-press button (text and filled variants)
  • CupertinoTextField -- rounded iOS text input with placeholder
  • CupertinoSwitch / CupertinoSlider -- iOS toggle and range controls
  • CupertinoAlertDialog -- rounded iOS alert with stacked actions
  • CupertinoActionSheet -- bottom action sheet with cancel button
  • CupertinoPicker / CupertinoDatePicker -- scroll wheel selection
  • Platform-adaptive widgets: Switch.adaptive, conditional platform checks

Practice Exercise

Build a complete iOS Settings screen clone using only Cupertino widgets. Include an Airplane Mode toggle, Wi-Fi and Bluetooth rows with navigation chevrons, a Notifications section, and a "Sign Out" action that shows a CupertinoActionSheet asking the user to confirm. Use CupertinoListSection.insetGrouped for the grouped layout.