Material Components (MDC) helps developers execute Material Design. Developed by a core team of engineers and UX designers at Google, MDC includes the same visual components Google uses in over 80 Apple AppStore apps. MDC has over 20 beautiful and functional iOS components and is available also for Android and Web.

This codelab shows situations where MDC can make your app more functional and beautiful, while saving engineering time.

What you will build

In this codelab, you're going to complete Shrine, an ecommerce app that sells clothing, home goods, and popsicles. Your app will:

  • Have an animated, stretchable navigation bar.
  • Allow the user to mark their favorite products.
  • Adjust layout to rotation.

Components you'll be using

What you'll need

This codelab is focused on using Material Components. There are mentions of non-relevant concepts and code blocks for you to simply copy and paste.

Download the codelab starter app (*Recommended method)

Download starter app

Or clone it from GitHub

git clone https://github.com/material-components/material-components-ios-codelabs.git

Open the workspace

  1. In the BuildingBeautifulApps directory, click into the Starter folder. (The Complete folder has the finished app.)
  2. Find the file BuildingBeautifulAppsObjcStarter.xcworkspace and open it.
  3. If Xcode was closed, it should open and load the BuildingBeautifulAppsObjcStarter.xcworkspace file.

Run the starter app

  1. Make sure the scheme to the right of the Run / Play button is BuildingBeautifulAppsObjcStarter and an iPhone simulator device is selected.
  2. Press the Run / Play button.
  3. Xcode will compile the app and open the iPhone simulator.
  4. The Shrine app should automatically open.

Voila! Our app is running in the simulator. You can change tabs at the bottom and scroll through individual products.

What's an App Bar?

If the designer gave you the following mocks, your job would be to update the app to match the new designs.

It's easy to make a UICollectionViewCell that looks like the mock on the left: just subclass UICollectionViewCell and add some subviews. But how do you keep it at the top of the screen at a fraction of its height like a navigation bar?

App Bar can do that. It has a built in MDCFlexibleHeaderView that watches the scrolling of the collection view and shrinks accordingly. When the App Bar gets to a minimum height (for example, 72 points), it freezes in place, allowing the collection, table, or scroll view to scroll beneath it. App Bar even adds a shadow.

Add App Bars with custom headers

Import the Material App Bar header:

ProductGridViewController.m

#import "MaterialAppBar.h"

Add a property for the App Bar, the custom header, and the logo to show when it is collapsed:

ProductGridViewController.m

@interface ProductGridViewController ()

//New code
@property(nonatomic) MDCAppBar *appBar;

//Existing code
@property(nonatomic) IBOutlet HomeHeaderView *headerContentView;
@property(nonatomic) IBOutlet UIImageView *shrineLogo;

@end

Add the App Bar in viewDidLoad:

ProductGridViewController.m

- (void)viewDidLoad {
  [super viewDidLoad];
  self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, self.tabBarController.tabBar.bounds.size.height, 0);

// New code
  self.appBar = [[MDCAppBar alloc] init];
  [self addChildViewController:self.appBar.headerViewController];
  [self.appBar addSubviewsToParent];
  self.appBar.headerViewController.headerView.trackingScrollView = self.collectionView;
  self.appBar.headerViewController.headerView.backgroundColor = [UIColor whiteColor];
  self.appBar.headerViewController.headerView.maximumHeight = 440;
  self.appBar.headerViewController.headerView.minimumHeight = 72;

  if (self.isHome) {
    [self setupHeaderContentView];
    [self setupHeaderLogo];
  }

  self.title = self.tabBarItem.title;

  // Existing code
  self.styler.cellStyle = MDCCollectionViewCellStyleCard;
  self.styler.cellLayoutType = MDCCollectionViewCellLayoutTypeGrid;
  self.styler.gridPadding = 8;
  [self updateLayout];
}

Setup the header:

ProductGridViewController.m

// New code
#pragma mark - Header

- (void)setupHeaderContentView {
  [self.appBar.headerViewController.headerView addSubview:self.headerContentView];
  self.headerContentView.frame = self.appBar.headerViewController.headerView.frame;
  self.headerContentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}

- (void)sizeHeaderView {
  MDCFlexibleHeaderView *headerView = self.appBar.headerViewController.headerView;
  CGRect bounds = [UIScreen mainScreen].bounds;
  if (self.isHome && bounds.size.width < bounds.size.height) {
    headerView.maximumHeight = 440;
  } else {
    headerView.maximumHeight = 72;
  }
  headerView.minimumHeight = 72;
}

- (void)setupHeaderLogo {
  [self.appBar.headerViewController.headerView addSubview:self.shrineLogo];
  [self.shrineLogo.topAnchor constraintEqualToAnchor:self.shrineLogo.superview.topAnchor constant:24].active = YES;
  [self.shrineLogo.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
  self.shrineLogo.translatesAutoresizingMaskIntoConstraints = NO;

  self.shrineLogo.alpha = 0;
}

Handle header size in updateLayout:

ProductGridViewController.m

- (void)updateLayout {
// New code 
 [self sizeHeaderView];

//Existing Code
  self.styler.gridColumnCount = 1;

  [self.collectionView.collectionViewLayout invalidateLayout];
}

Use UIScrollViewDelegate methods to animate a header transition and forward the methods on to the App Bar for the App Bar's custom scrolling behavior:

ProductGridViewController.m

// New code
#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  [self.appBar.headerViewController scrollViewDidScroll:scrollView];
  CGFloat scrollOffsetY = scrollView.contentOffset.y;
  CGFloat opacity = 1.0;

  if (scrollOffsetY > -240) {
    opacity = 0;
  }

  CGFloat logoOpacity = 0.0;

  if (scrollOffsetY > -240) {
    logoOpacity = 1.0;
  }

  [UIView animateWithDuration:0.2 animations:^{
    self.headerContentView.backgroundImage.alpha = opacity;
    self.headerContentView.descLabel.alpha = opacity;
    self.headerContentView.titleLabel.alpha = opacity;

    self.shrineLogo.alpha = logoOpacity;
  }];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  if (scrollView == self.appBar.headerViewController.headerView.trackingScrollView) {
    [self.appBar.headerViewController.headerView trackingScrollViewDidEndDecelerating];
  }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  MDCFlexibleHeaderView *headerView = self.appBar.headerViewController.headerView;
  if (scrollView == headerView.trackingScrollView) {
    [headerView trackingScrollViewDidEndDraggingWillDecelerate:decelerate];
  }
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                     withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset {
  MDCFlexibleHeaderView *headerView = self.appBar.headerViewController.headerView;
  if (scrollView == headerView.trackingScrollView) {
    [headerView trackingScrollViewWillEndDraggingWithVelocity:velocity
                                          targetContentOffset:targetContentOffset];
  }
}

Run the app:

Now there's a landing page header that slides away, condensing into just the logo in the navigation bar.

Now that we have have a flexible app bar, we can tackle the next problem: the designs call for two columns of products, not one.

Changing from one to two columns is simple with Material Collections.

Set the collection to two columns

In updateLayout, set the collections styler to 2 columns:

ProductGridViewController.m

- (void)updateLayout {
  [self sizeHeaderView];

  self.styler.gridColumnCount = 2; // New value

  [self.collectionView.collectionViewLayout invalidateLayout];
}

Run the app:

And voila! You've updated your grid layout to use two columns in about two seconds.

Material Collections can do lots of tricks:

  • Swipe-to-delete
  • Edit mode animations
  • Preset layouts

Since Material Components uses standard UICollectionViewDataSource methods, all you need to do is say how many columns you want.

The app looks great in portrait mode but not as good in landscape mode.

The two column format also doesn't look good on an iPad; users expect apps to take advantage of the extra screen real estate.

Luckily, Material Collections is easy to update when things change!

Handle trait changes

Delete the current updateLayout function and replace it with this new one:

ProductGridViewController.m

// New code
- (void)updateLayout {
  [self sizeHeaderView];
  
  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
    self.styler.gridColumnCount = 5;
  } else {
    switch (self.traitCollection.horizontalSizeClass) {
      case UIUserInterfaceSizeClassCompact:
        self.styler.gridColumnCount = 2;
        break;
      case UIUserInterfaceSizeClassUnspecified:
      case UIUserInterfaceSizeClassRegular:
        self.styler.gridColumnCount = 4;
        break;
    }
  }

  [self.collectionView.collectionViewLayout invalidateLayout];
}

Add two new functions (viewWillTransitionToSize and traitCollectionDidChange) above updateLayout:

ProductGridViewController.m

// New code
#pragma mark - Rotation and Screen size

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  [self updateLayout];
}

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  [self updateLayout];
}

// Existing code from the previous step
- (void)updateLayout {
  [self sizeHeaderView];
  
  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
    self.styler.gridColumnCount = 5;
  } else {
    switch (self.traitCollection.horizontalSizeClass) {
      case UIUserInterfaceSizeClassCompact:
        self.styler.gridColumnCount = 2;
        break;
      case UIUserInterfaceSizeClassUnspecified:
      case UIUserInterfaceSizeClassRegular:
        self.styler.gridColumnCount = 4;
        break;
    }
  }

  [self.collectionView.collectionViewLayout invalidateLayout];
}

Select an iPhone 7 Plus (or similarly sized) simulator device:

Run the app.

When the app loads, rotate the simulator by pressing command+[right or left arrow]:

With an iPad, there are five columns to show the collection. The user can browse every product at once.

Now that the app is easy to use, people are falling in love with your popsicles and binoculars. But the UX team has come back with a concern: the users don't know what the ♡ empty heart button does. When they press it, the heart icon fills in ♥. But what does that mean?

The filled-in heart icon shows that a user has just added a product to their favorites.

In app notifications

The designer has come up with a simple black bar that says "Added to favorites!" and she wants the black bar to:

  • Animate up from the bottom.
  • Dismiss itself after a couple seconds.
  • Allow the user to dismiss it by tapping it.

It looks simple, but it takes days to build the snackbar the way the designer has requested. This is the kind of problem that Material Snackbar can fix.

What's the Material Snackbar?

Snackbars are unobtrusive horizontal views that provide brief feedback about an operation through a message at the bottom of the screen. Maybe you've heard a snackbar called a "toast" or "toaster."

Material Components handles all snackbar presentation and manipulation through the MDCSnackbarManager: a lightweight singleton that parses the view hierarchy to perfectly place and animate a snackbar view with your message (and optionally, your call to action.)

MDCSnackbarManager also handles the snackbar queue. If you send five notifications in quick succession, that's no problem for MDCSnackbarManager!

Add the Material Snackbar

Start by importing MDC's Snackbar header:

ProductGridViewController.m

#import "MaterialSnackbar.h"

Update favoriteButtonDidTouch: to call the MDCSnackbarManager and tell it to showMessage:

ProductGridViewController.m

#pragma mark - Target / Action

- (void)favoriteButtonDidTouch:(UIButton *)sender {
  Product *product = self.products[sender.tag];
  product.isFavorite = !product.isFavorite;
  [self.collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:sender.tag inSection:0]]];

// New code
  if (product.isFavorite) {
    [MDCSnackbarManager showMessage:[MDCSnackbarMessage messageWithText:@"Added to favorites!"]];
  }
}

That's it! You don't have to know which view controller is being viewed or where the views are located. The MDCSnackbarManager looks at all the views in the hierarchy starting with the window until it finds the right one be the snackbar's superview.

Run the app, click on a heart icon and watch the snackbar perform:

Details

The snackbar is covering the bottom tab bar. The designer's mocks have the snackbar rising above the tab bar:

Set an offset for the snackbar:

AppDelegate.m

#import "MaterialSnackbar.h"

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  // Instantiate a UITabBarController with 3 ProductGridViewControllers
  NSString *const mainStoryboardName = @"ProductGrid";
  ProductGridViewController *home = [UIStoryboard storyboardWithName:mainStoryboardName bundle:nil].instantiateInitialViewController;
  home.isHome = YES;
  home.tabBarItem.titlePositionAdjustment = UIOffsetMake(0, -4);
  home.tabBarItem.title = @"Home";
  home.tabBarItem.image = [UIImage imageNamed:@"Diamond"];
  home.products = productsFor(ProductCategoryHome);

  ProductGridViewController *clothing = [UIStoryboard storyboardWithName:mainStoryboardName bundle:nil].instantiateInitialViewController;
  clothing.tabBarItem.titlePositionAdjustment = UIOffsetMake(0, -4);
  clothing.tabBarItem.title = @"Clothing";
  clothing.tabBarItem.image = [UIImage imageNamed:@"HeartFull"];
  clothing.products = productsFor(ProductCategoryClothing);

  ProductGridViewController *popsicles = [UIStoryboard storyboardWithName:mainStoryboardName bundle:nil].instantiateInitialViewController;
  popsicles.tabBarItem.titlePositionAdjustment = UIOffsetMake(0, -4);
  popsicles.tabBarItem.title = @"Popsicles";
  popsicles.tabBarItem.image = [UIImage imageNamed:@"Cart"];
  popsicles.products = productsFor(ProductCategoryPopsicles);

  UITabBarController *tabBarController = [[UITabBarController alloc] init];
  tabBarController.viewControllers = @[ clothing, home, popsicles ];
  tabBarController.tabBar.tintColor = [UIColor colorWithRed:96/255.0 green:125/255.0 blue:139/255.0 alpha:1];
  tabBarController.selectedIndex = 1;

  // Make the UITabBarController the main interface of the app
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  self.window.rootViewController = tabBarController;
  [self.window makeKeyAndVisible];

// New code
  [MDCSnackbarManager setBottomOffset:tabBarController.tabBar.bounds.size.height];

  return YES;
}

Now the snackbar looks much better!

By using Material Components for iOS, you now have the start of a beautiful e-commerce app that conforms to the Material Design guidelines and looks great across all devices. Your designer is happy, and you get to leave work early.

Next steps

There's so much more Material Components has to offer. Check out our site for documentation, code snippets, screenshots and links to the code.

MDC is also an open-source project! To contribute, read the Contributing Guidelines, sign the SLA and make a pull request.

For more guidance, frameworks and tools, check out material.io.