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
ZStack
to make our button the topmost view. Color.clear
expands to fill its container. In this case the entire screen.- The
overlay
modifier 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
minWidth
will allow our button to grow if we ever provide text and an icon. Depending on your use case,width
andheight
may suffice. - Set the icon color to reflect the background color. Create an extension on
Color
so 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)
}
}
ButtonStyle
requires 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
fab
is available as an extension onButtonStyle
where 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 previousZStack
code into this method and substitute in ourcontent
andalignment
properties.
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
ButtonModifier
to centralize our styling code - Leveraged static member lookup to create a convenient leading dot initializer for our style
- Created a
ViewModifier
to centralize our layout code - Extended
View
with a function that leverages the above to create and return us a configured FAB.