Reading time: 1 min

This recipe shows how to implement a floating action button (FAB) in SwiftUI. This is a simple component, common in Android apps, that sits in the bottom-right corner of the screen, floating above the rest of the content. The end result looks like this:

preview

The recipe goes as follows:

  • The FAB is implemented as a ViewModifier to allow for easy attachment to any content view.
  • ViewModifier's content is wrapped in a ZStack alongside the button, allowing it to be on top of the content.
  • Use a GeometryReader to position the button at the bottom-right corner of the screen.
struct FloatingActionButton<ImageView: View>: ViewModifier {
  let color: Color // background color of the FAB
  let image: ImageView // image shown in the FAB
  let action: () -> Void

  private let size: CGFloat = 60 // size of the FAB circle
  private let margin: CGFloat = 15 // distance from screen edges

  func body(content: Content) -> some View {
    GeometryReader { geo in
      ZStack {
        Color.clear // allows the ZStack to fill the entire screen
        content
        button(geo)
      }
    }
  }

  @ViewBuilder private func button(_ geo: GeometryProxy) -> some View {
    image
      .imageScale(.large)
      .frame(width: size, height: size)
      .background(Circle().fill(color))
      .shadow(color: .gray, radius: 2, x: 1, y: 1)
      .onTapGesture(perform: action)
      .offset(x: (geo.size.width - size) / 2 - margin,
              y: (geo.size.height - size) / 2 - margin)
  }
}

Lastly, add this helpful extension:

extension View {
  func floatingActionButton<ImageView: View>(
    color: Color,
    image: ImageView,
    action: @escaping () -> Void) -> some View {
    self.modifier(FloatingActionButton(color: color,
                                       image: image,
                                       action: action))
  }
}

Then, you can use it like this:

private struct FABTest: View {
  @State private var noRows = 9

  var body: some View {
    List(Array(1...noRows), id: \.self) { index in
      Text("Row \(index)")
    }
    .floatingActionButton(color: .blue,
                          image: Image(systemName: "plus")
                            .foregroundColor(.white)) {
      noRows += 1
    }
  }
}

Next Post Previous Post