FAB Specs
- Appears in front of all on-screen content
- Typically has a rounded shape and an icon in the center
- Usually positioned in the bottom-right corner of the screen
Positioning
ZStack { // 1
Text("Hello, world!")
Color.clear // 2
.overlay(alignment: .bottomTrailing) { // 3
Button(action: {}) {
Image(systemName: "plus")
}
.padding()
}
}- Place content in a
ZStackto make our button the topmost view. Color.clearexpands to fill its container. In this case the entire screen.- The
overlaymodifier allows us to appropriately position our button.
Styling
Button(action: {}) {
Image(systemName: "plus")
.padding([.leading, .trailing]) // 1
.frame(minWidth: 56, minHeight: 56) // 2
.foregroundColor(.systemBackground) // 3
.background(Color.accentColor) // 4
.cornerRadius(16) // 5
}
.padding() // 6
extension Color {
static var systemBackground: Color { Color(UIColor.systemBackground) }
}- Add space between the icon, and the container.
- Specifying
minWidthwill allow our button to grow if we ever provide text and an icon. Depending on your use case,widthandheightmay suffice. - Set the icon color to reflect the background color. Create an extension on
Colorso we can write it succinctly inline. - Set the background color
- Round the corners
- Add space between the button and the screen's edge.
Refactoring
That's it! We have everything we need to create a FAB. However, its pretty verbose. Let's explore how we can leverage features of Swift and SwiftUI to make it easier and more concise.
ButtonStyle
We can centralize our FAB styling code using SwiftUI's ButtonStyle protocol.
struct FabButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View { // 1
configuration.label // 2
.padding([.leading, .trailing])
.frame(minWidth: 56, minHeight: 56)
.foregroundColor(.systemBackground)
.background(.accentColor)
.cornerRadius(16)
}
}ButtonStylerequires us to implement themakeBody(configuration)function. We are given a configuration object that provides us the properties of the button.- Move our modifiers that we were applying to our button's image into our style and apply them to the button's label.
Now we can specify our style when we declare our button.
Button(action: {}) {
Image(systemName: "plus")
}
.buttonStyle(FabButtonStyle())
.padding()We introduced a minor regression. By specifying our button style, SwiftUI no longer automatically changes the appearance of our button when pressed. Luckily, the configuration object provides us the details we need to do it ourselves.
private let color: Color = .accentColor
func makeBody(configuration: Self.Configuration) -> some View {
// ...
.background(configuration.isPressed ? color.opacity(0.5) : color) // 1- If our button is pressed, we will set our background to 50% opacity.
Static Member Lookup
With a quick addition we can simplify our style even further.
extension ButtonStyle where Self == FabButtonStyle {
static var fab: Self { .init() } // 1
}- A static property
fabis available as an extension onButtonStylewhere the type adhering to it is ourFabButtonStyle
That mouthful allows us to declare our style within the buttonStyle modifier using a leading dot syntax.
.buttonStyle(.fab)ViewModifier
We can encapsulate the button's positioning logic in a ViewModifier.
struct Float<Child: View>: ViewModifier {
var alignment: Alignment // 1
@ViewBuilder var child: () -> Child
func body(content: Content) -> some View { // 2
ZStack {
content
Color.clear
.overlay(alignment: alignment) {
child()
}
}
}
}- When we create an instance of our modifier we will want to specify the alignment (ie.
.bottomTrailing) and the content we want displayed. - The protocol requires us to implement the
body(content:)method. We move our previousZStackcode into this method and substitute in ourcontentandalignmentproperties.
Now we can use our modifier directly on the screen's content.
Text("Hello, world!")
.modifier(
Float(alignment: .bottomTrailing) {
Button(action: {}) {
Image(systemName: "plus")
}
.buttonStyle(.fab)
.padding()
}
)Putting It All Together
We have centralized our styling code in FabButtonStyle and our positioning code in our Float view modifier. We can combine them together in an extension on View to make FAB's incredibly easy to create.
extension View {
func floatingActionButton(
_ systemName: String,
action: @escaping () -> () = {}
) -> some View {
Float(alignment: .bottomTrailing) {
Button(action: action) {
Image(systemName: systemName)
}
.buttonStyle(.fab)
.padding()
}
}
}Now we can simply call this function with the image name and action to perform when pressed and it'll configure our FAB and return it to us.
Text("Hello, world!")
.floatingActionButton("plus") {
print("success!")
}Summary
- Created a
ButtonModifierto centralize our styling code - Leveraged static member lookup to create a convenient leading dot initializer for our style
- Created a
ViewModifierto centralize our layout code - Extended
Viewwith a function that leverages the above to create and return us a configured FAB.