Alejandro Martinez

22 June, 2019SwiftSwiftUI🕐 5min read

SwiftUI reusable Button style

In my previous post about SwiftUI buttons I described a technique to reuse button configuration with a ViewModifier. Now that beta 2 is out, we finally have the proper tools to reuse styles in a much better way!

Styles with ViewModifier

Let's start by having a simple default SwiftUI button:

Button(action: {}) {
    Text("Default style")
}

In the previous post I used ViewModifier to encapsulate and reuse some styles. The problem with that technique is that you're forced to apply the modifier to the content of the button (SwiftUI calls this Label).

Button(action: {}) {
  	VStack {
        Image(...)
        Text("...")
    }
    .myButtonStyle() // <- my custom modifier
}

This was acceptable for a provisional workaround, but it's kind of weird to apply styles to the content of the button instead of the button itself.

And remember that we could not apply the modifier to the button itself because then things like padding and background were applied at the incorrect level of the view hierarchy. (read the previous post if you don't understand this because is a crucial aspect of SwiftUI)

 ButtonStyle

Luckily for us SwiftUI has a better way of doing this. A button has a modifier that accepts a ButtonStyle:

Button(action: {}) {
    Text("...")
}
.buttonStyle(.default)

We can use this ourselves and create our own custom styles as structs conforming to the ButtonStyle protocol.

public struct BackgroundButtonStyle: ButtonStyle {
    
    public func body(configuration: Button<Self.Label>, isPressed: Bool) -> some View {
        configuration
            .accentColor(.red)
            .padding()
            .background(.yellow)
            .cornerRadius(4)
    }
}

This follows a recurring pattern on SwiftUI, you create a struct conforming to a protocol. This is quite nice because it becomes familiar pretty quickly.

In this case the protocol is similar to ViewModifier. It defines a function body that accepts a configuration Button . This is the view to which you need to apply the modifiers.

It also has a second parameter, a boolean that tells us if the button is pressed or not. This is something that we don't have access with other styling techniques. It helps us change the style of the button based on the pressed state of it. As usual with SwiftUI, the body function will be called when this state changes, so you just need to declare the style based on it. SwiftUi takes care of the rest.

public struct BackgroundButtonStyle: ButtonStyle {
    public func body(configuration: Button<Self.Label>, isPressed: Bool) -> some View {
        configuration
            .accentColor(isPressed ? .yellow : .red)
            .padding()
            .background(isPressed ? Color.red : .yellow)
            .cornerRadius(4)
    }
}

In this example we change the background and accent colors of the button depending on the pressed state.

The question now is, how do we apply this style to the button?

 StaticMember

I'm not gonna lie, took me a while to figure out how this is supposed to work.

The first thing that you may try is to just pass the style to the modifier:

.buttonStyle(BackgroundButtonStyle())

But this doesn't work. The signature of that modifier expects a StaticMember:

.buttonStyle(StaticMember<ButtonStyle>)

But what is a static member?

The documentation describes it like this:

A concrete wrapper for enabling implicit member expressions.

Let me try to explain this. implicit member expressions are most commonly known by "dot syntax" on static members. This is when you can use .red in a method that accepts a UIColor. Because the compiler knows that the method expects a UIColor, and that this class has a static property called red that returns a UIColor itself, then it let us use this dot syntax without having to write UIColor at all.

The limitation with this is quite big. The compiler only looks for properties on the type of the parameter, and of course, it only accepts properties that return the same type.

This is quite an issue in some API designs. For example if you want your methods to accept protocols your users won't be able to use the implicit member expressions because the compiler won't look for static properties on the concrete types that implement the protocol.

In SwiftUI this is a big deal.

Think about the buttonStyle modifier for example. This modifier wants to accept a ButtonStyle, this makes it really flexible but also not that nice to use as you wouldn't be able to do .buttonStyle(.default).

To workaround this the framework provides a StaticMember, a simple generic type that helps to avoid this problem. You can see a detailed example on Apple's documentation.

SwiftUI declares the modifier to accept a StaticMember that wraps a ButtonStyle. It's just a workaround for the type system and the compiler. Hopefully in the future this won't be needed.

What it means for us is that in order to be able to use this custom style we need to accept this dance with the compiler 💃🏻. We need to extend StaticMember to add a static property that returns our style.

extension StaticMember where Base : ButtonStyle {
    public static var background: BackgroundButtonStyle.Member {
        StaticMember<BackgroundButtonStyle>(BackgroundButtonStyle())
    }
}

As an aside, I'm omitting the return in this single line function. Watch my video about Swift's new Implicit Return if you want to know more.

Now that we have all the pieces in place we're free to use our custom style!

Button(action: {}) {
    Text("Background style")
}
.buttonStyle(.background)

 Combining styles

The nice thing about styles being just views that wrap other views is that we benefit from their composability.

Let's make another custom style. Just for fun and to show you how easy is to do nice effects with SwiftUI, let's make the button scale when pressed.

public struct ScaledButtonStyle: ButtonStyle {
    public func body(configuration: Button<Self.Label>, isPressed: Bool) -> some View {
        configuration
            .scaleEffect(isPressed ? 1.4 : 1)
            .animation(Animation.spring().speed(2))
    }
}

Now you can combine both styles as you combine any other view in SwiftUI.

Button(action: {}) {
    Text("Background style")
}
.buttonStyle(.scaled)
.buttonStyle(.background)

Take a moment to appreciate how nice an powerful this is.

Conclusion

Making custom styles in SwiftUI is as easy as making normal views. SwiftUI uses the same pattern of structs implementing a protocol that returns a body View for many of its APIs.

In this case it's a little more complicated because we need to use this StaticMember workaround. But once you know it is quite easy to repeat. And the final syntax is as nice as it can be 😊 .

A concern that I have, with beta 2 at least, is that Xcode is not able to suggest this static members yet. So even if .buttonStyle(.background) compiles, it's quite obscure to figure out what styles you have available. Hopefully is fixed in upcoming betas.

👉🏻 If you have any feedback you can reach me at alexito4

RSS

Alejandro Martinez
alexito4@gmail.com

Buka pintu