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()
        }
}
  1. Place content in a ZStack to make our button the topmost view.
  2. Color.clear expands to fill its container. In this case the entire screen.
  3. 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) }
}
  1. Add space between the icon, and the container.
  2. Specifying minWidth will allow our button to grow if we ever provide text and an icon. Depending on your use case, width and height may suffice.
  3. Set the icon color to reflect the background color. Create an extension on Color so we can write it succinctly inline.
  4. Set the background color
  5. Round the corners
  6. 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)
    }
}
  1. ButtonStyle requires us to implement the makeBody(configuration) function. We are given a configuration object that provides us the properties of the button.
  2. 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
  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
}
  1. A static property fab is available as an extension on ButtonStyle where the type adhering to it is our FabButtonStyle

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()
                }
        }
    }
}
  1. When we create an instance of our modifier we will want to specify the alignment (ie. .bottomTrailing) and the content we want displayed.
  2. The protocol requires us to implement the body(content:) method. We move our previous ZStack code into this method and substitute in our content and alignment 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.