Using Material Design Components in SwiftUI

The mobile development scene is slowly shifting towards declarative UIs, but large and popular UI libraries are understandably still using the old UI paradigms. An example of this is Google’s Material Components for iOS library.

Therefore, if we want to use Material design in our iOS app that we are building in SwiftUI we can either:

  1. Implement the components ourselves from scratch
  2. Find a way to use the existing Material Components library

We’ll be looking at the second option. We need a way to integrate the UIKit views provided by the library to be used in SwiftUI, and we can use UIViewRepresentable to do exactly that.

Our approach

For this example we will be using a floating action button, but a similar approach can be used for other views in the Material Components library.

The library implements floating action buttons using MDCFloatingButton which is a UIKit view or UIView. We will wrap this in our own SwiftUI representation that we will call FloatingActionButton. The aim here is to build the FloatingActionButton in such a way that will mirror the behaviour of the native SwiftUI views as much as possible.

Using UIViewRepresentable

If you are following along, start by following these instructions about getting your project setup with the Material components library.

Once setup, the instructions state that in the UIKit world we can simply call:

let fab = MDCFloatingButton()

Let’s look at how we can use this in a UIViewRepresentable such that we can integrate it in SwiftUI.

We will begin with the building the button and adding a plus sign to it. Ideally we would do this with an icon, but here we will use the string “+” to keep things simple. First we will create a file named FloatingActionButton.swift and add the following:

We override the two required functions, makeUIView(context:) which is where we create the MDCFloatingButton view, and updateUIView(_:context:) which updates the state of the view with new information from SwiftUI. For now we are just updating the title.

Next we can add the FloatingActionButton to our content view, just like any other SwiftUI view. We can use a combination of VStack, HStack and Spacer to position our button in the bottom right corner of the screen.

Image for post
Image for post

Excellent, we’ve successfully created our floating action button in SwiftUI. But what’s the use of a button if you can’t tap on it? Let’s look at how we can handle taps.

Out of the box, UIViewRepresentable doesn’t automatically communicate any changes or interactions to the rest of the SwiftUI interface. In order to accomplish this, we can make use of a Coordinator.

Similar to the SwiftUI Button view, we want to allow a closure to be passed into the FloatingActionButton’s constructor. We will add a target-action pair to the MDCFloatingButton, using the Coordinator to invoke that closure whenever the user taps the button.

Let’s take a look at the code:

Now we can pass a closure into the view declaration to respond to users’ taps:

Great, we now have a button that actually works like a button!

So far, our button looks a bit boring. Let’s customise the appearance a little bit. First we’ll look at how we can make the plus sign a bit bigger.

In SwiftUI, we make use of view modifiers to change the appearance of a view. There are many pre-made modifiers, one of which is the font(_:) view modifier. Let’s try to apply that to our FloatingActionButton:

Easy, huh? Not so fast. If we rebuild the app now, we’ll actually see no change to our button. Why doesn’t it work? 🤔

We are telling the view that we want to apply a large title to it, but it’s not listening! The problem is that the UIViewRepresentable doesn’t know how to handle the value provided in the view modifier.

To fix this we will make use of the Environment property wrapper to access this value. This is what it looks like:

@Environment(\.font) var font: Font?

Here we encounter our next problem. MDCFloatingButton only knows how to apply a UIFont, but the value that we receive from the view modifier is a SwiftUI Font. There is currently no built-in way to convert between these types, so we will use an extension function to help us out here.

Now we have a UIFont that we can apply to the MDCFloatingButton:

And now when we build and run the app, we can see the plus looks a bit bigger.

Image for post
Image for post

Our button is looking a bit better now, but it would be nice if we could change the colour too. Let’s see how we can change it to blue.

At this point, it would be reasonable to think that we could apply the same technique that we used to customise the font to also customise the colour. There is a view modifier named foregroundColor(_:) where we can pass in a SwiftUI Color. However, when we try to access the foreground colour using the Environment property wrapper we are told:

foreground color is inaccessible due to internal protection level
foreground color is inaccessible due to internal protection level

The reason for this is unclear and it would be nice if Apple could address this in a future release, but for now we will have to look for an alternative solution.

Solution #1

One approach is we could simply pass the desired colour into the FloatingActionButton’s constructor and then apply that colour to the MDCFloatingButton — thankfully this time UIColor has a constructor which accepts a SwiftUI Color. 😀

This works, but it’s not very SwiftUI-like. As we’ve seen, SwiftUI uses view modifiers to customise the view’s appearance. This is where we can instead use a custom view modifier.

Solution #2

View modifiers implement the ViewModifier protocol and should be able to be applied to any view. In our situation, the modifier we want to implement needs to know specific information about the FloatingActionButton, namely that we need to set the backgroundColor property which we have declared. In this sense, what we will implement here is not a true view modifier per se, but it will work for our use case.

Implementing the backgroundColor(_:) modifier as an extension function:

One caveat of this approach is that our view modifier will have to appear before the actual view modifiers because ours can only be applied to the FloatingActionButton type. Built-in view modifiers or any custom view modifiers implementing the ViewModifier have an opaque return type of some View.

And now let’s build and run the app for a final look at the button we’ve built:

Image for post
Image for post

Conclusion

As we’ve seen, this is not a perfect solution. There are workarounds we’ve had to use and drawbacks to some of these approaches. But until such time that Google updates their library, this will hopefully provide you with some direction to allow you to do something similar for other components in the Material Components library or any other views built in UIKit.

Written by

Mobile Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store