Swift and SwiftUI tutorials for Swift Developers

Floating Action Button in SwiftUI

In the current mobile app development ecosystem, usability and visual hierarchy are fundamental. For any iOS Developer looking to stand out, mastering modern design patterns is a must. One of the most recognizable and useful elements is the Floating Action Button (FAB).

Although originally popularized by Material Design, the FAB has found its home in the Apple ecosystem, facilitating primary actions like composing an email, starting a tweet, or adding a task. In this comprehensive tutorial, you will learn step-by-step how to create a floating action button in SwiftUI, optimizing your workflow in Xcode and applying Swift programming best practices.

Why use a FAB in SwiftUI?

Before writing code, let’s understand the theory. A FAB is a circular button that floats above the interface content. Its goal is to promote the screen’s primary action. When developing in SwiftUI, the implementation differs drastically from UIKit; here we leave complex constraints behind to embrace the power of Stacks and Overlays.

Part 1: Basic Implementation with ZStack

To get started in Xcode, we need to understand the ZStack container. Unlike vertical or horizontal stacks, ZStack allows us to layer views on the depth axis (Z). The classic strategy for a beginner developer is often to use Spacers to push the button into a corner.

Let’s look at the first basic code example:

import SwiftUI

struct BasicFABView: View {
    var body: some View {
        ZStack {
            // Layer 1: The Main Content (Sample List)
            NavigationView {
                List(0..<30) { item in
                    Text("List item #\(item)")
                        .padding()
                }
                .navigationTitle("Home")
            }
            
            // Layer 2: The Floating Action Button (FAB)
            VStack {
                Spacer() // Pushes everything to the bottom
                HStack {
                    Spacer() // Pushes everything to the right
                    
                    Button(action: {
                        print("FAB pressed: Primary Action")
                    }) {
                        Image(systemName: "plus")
                            .font(.title.weight(.semibold))
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .clipShape(Circle())
                            .shadow(radius: 4, x: 0, y: 4)
                    }
                    .padding() // Margin from the safe edge of the screen
                }
            }
        }
    }
}

This approach works, but as an expert iOS Developer, you’ll notice that cluttering the view with Spacers can make the code messy and hard to read, especially in complex views.

Part 2: The Professional Way using .overlay

A much cleaner and more powerful technique in modern Swift programming is using the .overlay() modifier along with the alignment property. This links the button to the parent container without needing extra ZStack structures wrapping the entire screen.

struct ProfessionalFABView: View {
    var body: some View {
        NavigationView {
            List(0..<20) { i in 
                Text("Pending Task \(i)") 
            }
            .navigationTitle("My Tasks")
            // Applying SwiftUI magic here
            .overlay(
                Button(action: {
                    // Creation logic here
                }) {
                    Image(systemName: "plus")
                        .font(.title2)
                        .frame(width: 60, height: 60) // Fixed size for touch area
                        .background(Color.accentColor)
                        .foregroundColor(.white)
                        .clipShape(Circle())
                        .shadow(color: .black.opacity(0.3), radius: 5, x: 3, y: 3)
                }
                .padding() // Padding to separate from the edge
                , alignment: .bottomTrailing // Automatic alignment
            )
        }
    }
}

Part 3: Creating a Reusable Component

The DRY (Don’t Repeat Yourself) principle is vital. If you are going to create a floating action button in SwiftUI on multiple screens, ideally you should encapsulate it in a reusable structure. This allows you to change the design in one place and have it reflect across your entire app.

import SwiftUI

struct FloatingActionButton: View {
    // Configurable properties for maximum flexibility
    var icon: String = "plus"
    var backgroundColor: Color = .blue
    var foregroundColor: Color = .white
    var action: () -> Void
    
    var body: some View {
        Button(action: action) {
            Image(systemName: icon)
                .font(.system(size: 24))
                .foregroundColor(foregroundColor)
                .frame(width: 60, height: 60)
                .background(backgroundColor)
                .clipShape(Circle())
                .shadow(color: Color.black.opacity(0.3), radius: 5, x: 3, y: 3)
                .contentShape(Circle()) // Improves touch area
        }
        .padding()
    }
}

Part 4: Expandable FAB (Animated Menu)

Let’s take this to the next level. Often, a single button isn’t enough. We need a dropdown menu (like a “Speed Dial”). This is where SwiftUI shines with its declarative animations.

We will use @State to control visibility and withAnimation to smooth the transition.

struct ExpandableFAB: View {
    @State private var isExpanded = false
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            // Dummy background
            Color.gray.opacity(0.1).ignoresSafeArea()
            
            VStack(spacing: 20) {
                if isExpanded {
                    // Secondary action buttons
                    menuButton(icon: "camera.fill", label: "Camera") { print("Camera") }
                    menuButton(icon: "photo.fill", label: "Gallery") { print("Gallery") }
                }
                
                // Main Button
                Button(action: {
                    withAnimation(.spring(response: 0.4, dampingFraction: 0.6)) {
                        isExpanded.toggle()
                    }
                }) {
                    Image(systemName: "plus")
                        .font(.title2)
                        .rotationEffect(.degrees(isExpanded ? 45 : 0)) // Rotates to an 'X'
                        .foregroundColor(.white)
                        .frame(width: 60, height: 60)
                        .background(Color.blue)
                        .clipShape(Circle())
                        .shadow(radius: 5)
                }
            }
            .padding()
        }
    }
    
    // Helper for small buttons
    func menuButton(icon: String, label: String, action: @escaping () -> Void) -> some View {
        HStack {
            Text(label)
                .font(.caption)
                .padding(5)
                .background(Color.white)
                .cornerRadius(5)
                .shadow(radius: 2)
            
            Button(action: action) {
                Image(systemName: icon)
                    .padding()
                    .background(Color.white)
                    .foregroundColor(.blue)
                    .clipShape(Circle())
                    .shadow(radius: 3)
            }
        }
        .transition(.scale.combined(with: .move(edge: .bottom))) // Combined transition
    }
}

Part 5: Multiplatform Adaptation (macOS and watchOS)

A true iOS Developer knows that SwiftUI code can run on multiple devices, but the User Experience (UX) must adapt. A giant button in the corner doesn’t work the same way on a Mac as it does on a watch.

Adaptation for macOS

On macOS, we need to handle the hover state (when the mouse passes over) and remove the default button styles that add grey borders.

#if os(macOS)
struct MacFAB: View {
    @State private var isHovering = false
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            Color.white // Content
            
            Button(action: { print("Action on Mac") }) {
                Image(systemName: "plus")
                    .font(.title)
                    .foregroundColor(.white)
                    .frame(width: 50, height: 50)
            }
            .buttonStyle(.plain) // CRUCIAL on macOS
            .background(isHovering ? Color.blue.opacity(0.8) : Color.blue)
            .clipShape(Circle())
            .shadow(radius: 4)
            .padding(20)
            .onHover { hovering in
                withAnimation(.easeInOut(duration: 0.2)) {
                    isHovering = hovering
                }
            }
        }
        .frame(minWidth: 400, minHeight: 300)
    }
}
#endif

Adaptation for watchOS

On the Apple Watch, corners are dangerous and space is limited. It is often better to place the button at the end of a ScrollView or use the .toolbar, but if you need a floating button, ensure it doesn’t cover content.

struct WatchFAB: View {
    var body: some View {
        ZStack {
            List(0..<10) { _ in Text("Data...") }
            
            VStack {
                Spacer()
                Button(action: {}) {
                    Image(systemName: "mic.fill")
                        .font(.title3)
                }
                .frame(width: 50, height: 50)
                .background(Color.red)
                .clipShape(Circle())
                .buttonStyle(.plain) // Avoids the full list row style
                .padding(.bottom, 5)
            }
        }
    }
}

Accessibility: The Professional Touch

Don’t forget accessibility. A button containing only an icon can be invisible to users relying on VoiceOver. In Xcode, you can easily test this with the accessibility inspector.

Button(action: {
    // Action
}) {
    Image(systemName: "plus")
}
.accessibilityLabel("Create new note") // What VoiceOver reads
.accessibilityHint("Opens the editor to write a new note") // Additional help
.accessibilityAddTraits(.isButton)

Conclusion

Knowing how to create a floating action button in SwiftUI is an essential skill combining design and functionality. We have journeyed from basic implementation with ZStack to advanced solutions with animated menus and multiplatform support.

As an iOS Developer, your goal should always be to write clean and maintainable code. Using modifiers like .overlay and extracting reusable components not only improves the readability of your project in Xcode, but also facilitates future scalability of your applications. Now it’s your turn to implement these concepts in your next big project!

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

How to Detect Dark Mode in SwiftUI

Next Article

Foundation Models with Swift

Related Posts