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

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

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.

First steps

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.

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.

Handling taps

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.

Updating the font

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.

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.

Customising the colour

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:

Conclusion

Mobile Developer