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:
- Implement the components ourselves from scratch
- 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.
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.
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
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.
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
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
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
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
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
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:
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.
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
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.
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.
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
And now let’s build and run the app for a final look at the button we’ve built:
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.