Flutter: How to implement Cupertino UI (iOS Style), with examples!

Flutter: How to implement Cupertino UI (iOS Style), with examples!

In-depth explanation all about how to implement CupertinoUI (iOS) using Flutter on a real project! Get the project's code template and design for free

ยท

9 min read

Featured on Hashnode

Hello, folks of the developer community ๐Ÿ‘‹!

I'm back with the promised article on Flutter ๐Ÿ”ฅ, specifically in the Cupertino UI, or you may know it as the iOS-style implementation. For those who aren't familiar with Flutter, it's a framework by Google that provides an elegant UI Toolkit for multiplatform app development. Since its first release, Flutter has gained a lot of attention from the developer community. Thus, expanding its capabilities, in this context, various UI packages for developing a specific platform.

In my Flutter learning progress, I tend to build usable projects out of a specific topic I want to cover. This time, out of my fascination with Apple Design (Human Interface Guideline) and combined with my favorite music playlist channel on YouTube: Leeplay, I want to build a music player app with the native iOS looks.

Thankfully, the official Flutter team has provided the Cupertino package, which contains iOS-style widgets and behaviors. After I developed the music player app, I want to share my experience, references, and an in-depth explanation of each iOS widget used in the project. Moreover, you may refer to the official docs for more code examples and class properties.

Article 1 Asset.png ๐Ÿ’ก Get the free UI Kit of Leeplay the music player app project here

Based on the HIG, I have designed the music player app called Leeplay. You might notice that I used a unique primary color to stand out from the original Apple apps like Apple Music, which used Apple's color palettes. As I promised in my first intro post, I will share the design asset and the design reference (in Figma) for the Leeplay project. Cheers ๐Ÿป!

Now, after the app's design intro, let's jump into the coding, and I will explain each widget I used along the way.

  1. Before initializing the project, I highly recommend using macOS for the development process where it has an iOS simulator and doesn't require any additional setup than Windows (Android). But again, our concern here is regarding the UI, thus allowing you to develop using Windows and its Android emulator.
  2. As usual, initialize the project using Flutter's command line itself.
  3. The first thing we're going to do is change the default MaterialApp parent widget into CupertinoApp. Essentially, this widget will render a global iOS behavior across the app. Moreover, we could define other global behaviors like routes, theme, app's title, disabling debug banner, and the usual home widget.
  4. Let's continue working on the theming part. We will fill the theme property in CupertinoApp with the CupertinoThemeData class. Before getting into each property, I must highlight that the Cupertino package has provided many Cupertino-styled colors called the CupertinoColors class. As shown in the example below, the ThemeData class contains several theming properties such as:
    • brightness is for rendering light or dark based on provided themes.
    • The primaryColor is pretty self-explanatory, where I used my accent color from the UI design asset.
    • The scaffoldBackgroundColor is for the CupertinoScaffold widget's background color.
    • textTheme property is for specifying a whole bunch of text use-cases across the app, typically based on some iOS components. In this case, we will use CupertinoTextThemeData, but I will only fill the default property. For the rest, I will create a static class with sets of properties with semantic naming and fill it with the TextStyle widget that follows the HIG. Using a custom static class like this is faster than calling the root CupertinoTheme via the current context. ๐Ÿ’ก Another bonus! I attached the custom TextTheme class code. Download it here!
      CupertinoApp(
       debugShowCheckedModeBanner: false,
       title: 'Leeplay',
       theme: CupertinoThemeData(
                 brightness: (state is LightTheme) ? Brightness.light : Brightness.dark,
                 primaryColor: const Color(0xFF00B903),
                 scaffoldBackgroundColor: state is LightTheme ? CupertinoColors.white : CupertinoColors.darkBackgroundGray,
                 textTheme: CupertinoTextThemeData(
                      textStyle: TextStyle(
                          fontFamily: "SFPro",
                          color: state is LightTheme
                          ? CupertinoColors.black
                          : CupertinoColors.white,
                      ),
               ),
      ), 
      home: ...,
      )
      
  5. Next on, we will continue slicing the UI pages. But, instead of elaborating on the process, I will only highlight the definition and implementation of every iOS-specific widget and package.

    • Scaffold.

      Flutter provides several iOS-styled widgets to scaffold an entire page. The basic one is CupertinoScaffoldPage, where we can modify its background color, child, and navigation bar. Just think of this one as the usual Scaffold widget. If you want to implement the tabbed views, we can use CupertinoTabScaffold. This widget is robust because we can define our custom BottomNavigationBar widget and set up the logic of displaying pages or the scaffold's children according to the current context and active index.

      CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.music_note_2, size: 22),
            label: "Play"
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search, size: 24),
            label: "Search"
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.square_stack, size: 22),
            label: "Library"
          )
        ],
      ),
      tabBuilder: (BuildContext context, int index) {
        return CupertinoTabView(
          builder: (BuildContext context) {
            if(index == 0){
              return const Homepage();
            } else if (index == 1){
              return const SearchPage();
            } else if (index == 2){
              return const SettingsPage();
            } else {
              return const Homepage();
            }
          },
        );
      },
      );
      
    • Routes and Navigator widget.

      The routing and Navigator class of Flutter is still working pretty much the same. But, iOS got a little behavior difference, where we can swipe right to go back. Regarding widgets, we got the CupertinoNavigationBar, where we can modify many properties. But here, I only modified the leading icon, removing the default title and expanding the trailing size to contain two icons.

      CupertinoNavigationBar(
        leading: CupertinoButton(
          minSize: 0,
          padding: EdgeInsets.zero,
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Icon(CupertinoIcons.back, size: 22),
        ),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Padding(
              padding: const EdgeInsets.only(right: 20.0, top: 2.0),
              child: CupertinoButton(
                minSize: 0,
                padding: EdgeInsets.zero,
                onPressed: () {
                  Navigator.pop(context);
                },
                child: const Icon(CupertinoIcons.heart, size: 24),
              ),
            ),
            CupertinoButton(
              minSize: 0,
              padding: EdgeInsets.zero,
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Icon(CupertinoIcons.share, size: 22),
            ),
          ],
        ),
      ),
      

      The other one is the CupertinoTabBar which should be assigned only on CupertinoTabScaffold as its bottom navbar. As usual, it's possible to modify any of its properties (height, background color, icon size, etc.), but by default, the widget already looks like the native iOS bottom navbar. In this project, I assigned several BottomNavigationIcon according to how many CupertinoTabScaffold's children. Despite the BottomNavigationIcon widget being from the Flutter core widgets, it works pretty well here.

      CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.music_note_2, size: 22),
            label: "Play"
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search, size: 24),
            label: "Search"
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.square_stack, size: 22),
            label: "Library"
          )
        ],
      ),
      
    • Modal.

      Cupertino design got several modals types: ActionSheet, AlertDialog, ContextMenu, and BottomSheet. In Flutter, aside from the ContextMenu widget, we can use the showCupertino...() methods to display these modal widgets. Then, we must assign the desired modal like ActionSheet in the method's builder. In this project, I only use the BottomSheet from the modal_bottom_sheet package. For some detailed examples of the official modal widgets, you may refer to these links.

      showCupertinoModalBottomSheet(
         context: context,
         builder: (context) => const AboutLeeplayBottomSheet()
      );
      
    • Button.

      The next widget is CupertinoButton, which I used many times across the app. The button acts similarly to Material button, where you can provide pretty much all widgets as its child and VoidCallbackas its onPressed method. By default, this button will have no background and a rectangular size (its minSize property). But, we can easily add background color via its property or a shortcut. Moreover, we can specify the button's padding, minSize (default wide), and the splash opacity (the iOS button will be fading in and out if tapped).

      CupertinoButton.filled(
        child: Center(
          child: Text(
            "Get started using NAVER",
            style: AppText.body.copyWith(
              fontWeight: FontWeight.w600,
              height: 1,
              color: CupertinoColors.white,
            ),
          ),
        ),
        pressedOpacity: 0.8,
        onPressed: () async {
          context.read<AuthCubit>().loginWithLine();
        },
      ),
      

      A little tip for me, if you want to mimic the Material's IconButton widget, we could modify the minSize property to 0, where the CupertinoButton's size will adapt to its child's size (the Icon widget).

      CupertinoButton(
          minSize: 0,
          padding: EdgeInsets.zero,
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Icon(CupertinoIcons.heart, size: 24),
      ),
      
    • Text Field.

      Flutter Cupertino provides two types of text field. The first one is the CupertinoSearchTextField widget which provides a native look to the iOS search bar. But to get it tidy and pretty symmetric, I must modify its padding value, icon size, and icon spacing value a bit.

      CupertinoSearchTextField(
         onChanged: ((value) {}),
         placeholder: "Search Leeplay's playlists from YouTube",
         padding: const EdgeInsets.all(12).copyWith(top: 10),
         prefixInsets: const EdgeInsetsDirectional.only(start: 12),
         suffixInsets: const EdgeInsetsDirectional.only(end: 12),
      )
      

      The second one is the default CupertinoTextField, which sadly I didn't use in this music app. Still, it behaves similarly to the default TextField, where can specify its style, padding, and even disable the suggestion feature on the iOS keyboard. For more info on CupertinoTextField class, you may refer to it here.

    • Loading Indicator.

      Another widget in this project is the iOS loading indicator or CupertinoActivityIndicator in Flutter widgets. By default, this widget will render a grayish loading indicator, where we can modify its color and radius (circular size).

      const CupertinoActivityIndicator(
          radius: 16.0,
      ),
      
    • Settings UI.

      For the settings page, I used a pub.dev package called flutter_cupertino_settings. This package is useful if you want to render an iOS-styled settings UI, where it provides several widgets like CSHeader, CSControl (slider), and CSSelection (dropdown), and CSSwitch (switch) to speed up the development. To get started, wrap your entire setting widgets under the CupertinoSettings(). Under the hood, these custom widgets still use some basic widgets like Slider and Switch, which you can read the docs here. Unfortunately, some of its styles are inconsistent, but they can be resolved manually. Still worth the time!

  6. Finally, I've summarized all of the iOS / Cupertino widgets and packages that I've used across this project. There are indeed some other Cupertino widgets I haven't used in the Leeplay project, but I recommend you dive into the Flutter official docs on those widgets. As the final step of development, I will release this iOS-styled app. Whether you want to release it for the iOS platform or even force it to the Andoird platform, the production build process is the same and simple as explained in the Flutter docs here.

In retrospect, I'm confident that Flutter is robust enough to build an iOS app with its native looks ๐Ÿ”ฅ. But, honestly, with my little background in SwiftUI development, I think Flutter is still outclassed by the native development tool. Fortunately, the development experience using Flutter is quite exceptional. I appreciate that Flutter still carries its big value of multiplatform where their core widgets are perfectly usable and basically can render anything.

Thanks for reading folks, I'm open to any of your feedback and I do hope that the UI design file and the code template could be useful for you guys!

Cheers, Andre ๐Ÿ‘‹.

ย