Custom Toggle (Switch) in SwiftUI

XavierXavier
4 min read

Hi there, long time no see! Today I’m going to demonstrate how to create a custom toggle in SwiftUI from scratch.

With a custom toggle, you have the freedom to imbue your interface with a distinctive personality that aligns perfectly with your brand or app theme. Beyond aesthetics, customization grants you precise control over the behavior and animations of the toggle, ensuring a seamless and engaging user experience.

Without further ado, let’s get started.

PS: The code in this post is available here.

Step 1: Set up basic toggle view skeleton and content view for preview

  1. Create a ToggleView struct that conforms to View.

  2. In side Toggle View’s body, create a HStack that contains a Text, a Spacer and a RoundedRectangle.

  3. Add a text property that allows we pass a text to the toggle.

  4. Make the corner radius for the rounded rectangle large to make the edges smoother. You can also replace it with a Capsule if you prefer.

  5. Add a binding bool called isOn, which reads and writes a bool from outside.

  6. Alter the foreground color of the rounded rectangle based on isOn, using an ternary operator isOn ? onColor : offColor. Ideally the color should be configurable, for this tutorial, we’ll keep it simple and use .orange and .accentColor.

    struct ToggleView: View {
        let text: LocalizedStringKey
        @Binding var isOn: Bool
        var body: some View {
            HStack {
                Text(text)
                Spacer()
                RoundedRectangle(cornerRadius: 50)
                    .foregroundColor(isOn ? .orange : .accentColor)
                    .frame(width: 51, height: 31)
                    .onTapGesture { isOn.toggle() }
            }
        }
    }

In the content view, create two toggles and pass a constant true and false for the binding so that we can easily see how the view looks when toggle is on and off at the same time.

    struct ContentView: View {
        var body: some View {
            VStack {
                ToggleView(text: "Custom Toggle On", isOn: .constant(true))
                ToggleView(text: "Custom Toggle Off", isOn: .constant(false))
            }
            .padding()
        }
    }

Below is how it looks as of now. Next step is to add a circle to indicate the toggle state (on or off).

Step 2 - Circle Overlay

To show if the toggle is on or off, we need a circle (or another shape you prefer) a horizontal offset.

  1. Add a computed property that determines the offset based on isOn. i.e. var circleOffset: CGFloat { isOn ? 11: -11 }

  2. Add an overlay for the rounded rectangle. Inside the overlay, add a circle with x offset of the offset defined above.

    struct ToggleView: View {
        let text: LocalizedStringKey
        @Binding var isOn: Bool
        // -------------- New in Step 2
        private var circleOffset: CGFloat {
            isOn ? 11 : -11
        }
        var body: some View {
            HStack {
                Text(text)
                Spacer()
                RoundedRectangle(cornerRadius: 50)
                    .foregroundColor(isOn ? .orange : .accentColor)
                    .frame(width: 51, height: 31)
                    // -------------- New in Step 2
                    .overlay(
                        Circle()
                            .frame(width: 25,
                                   height: 25)
                            .foregroundColor(.white)
                            .padding(3)
                            .offset(x: circleOffset)
                    )
            }
        }
    }

Now the circle will pushed to right hand side when toggle is on and left hand side if toggle is off.

Step 3 - Image Overlay for the Circle (Optional)

This step is optional. If you want, you can also provide a small icon as an overlay of the circle. This icon will definitely look better if its color is changing based on the toggle state.

Below is an example using "checkmark" and "xmark" SF symbol.

Circle()
    .frame(width: 25,
           height: 25)
    .foregroundColor(.white)
    .padding(3)
    // -------------- New in Step 3
    .overlay(
        Image(systemName: isOn ? "checkmark" : "xmark")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .font(.title.weight(.bold))
            .frame(width: 10,
                   height: 10)
            .foregroundColor(isOn ? .orange : .accentColor)
    )
    .offset(x: circleOffset)

Step 4 - Simple but important logic

As a toggle, don’t forget to change the binding bool value when it’s tapped.

So add an onTapGesture for the RoundedRectangle. To ensure smooth transition, we can also give it a faster animation.

RoundedRectangle(cornerRadius: 50)
    .foregroundColor(isOn ? .orange : .accentColor)
    .frame(width: 51,
           height: 31)
    .overlay(
        Circle()
            .frame(width: 25,
                   height: 25)
            .foregroundColor(.white)
            .padding(3)
            .overlay(
                Image(systemName: isOn ? "checkmark" : "xmark")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .font(.title.weight(.bold))
                    .frame(width: 10,
                           height: 10)
                    .foregroundColor(isOn ? .orange : .accentColor)
            )
            .offset(x: circleOffset)
    )
    // -------------- New in Step 4
    .animation(.linear(duration: 0.15), value: isOn)
    .onTapGesture { isOn.toggle() }

And that’s it! Our toggle is done.

One more thing, in this tutorial, we hardcoded the on and off colors, label as a text etc, actually these can all be a parameter so that the toggle is more customizable, and you can then pass a view as the label instead of a string/localized string key, or pass two colors for design the rounded rectangle color, or even the image/icon on the circle. But I believe this post can give you an idea of the basic customization.

That’s everything for this post.

Don’t forget to subscribe to my newsletter if you’d like to receive posts like this via email. If you like my posts, 😚consider tipping me at buymeacoffee.com/xavierios.

0
Subscribe to my newsletter

Read articles from Xavier directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Xavier
Xavier

iOS developer from Toronto, ON, CAN