You can get quite a fluid, bouncy animation if you use a GlassEffectContainer to hold the two variations of the button and then switch between them. It is important that changes to the flag are performed withAnimation.
The example below shows the effect you get. This example is using .glass as button style, but it also works if you apply the glass effect yourself, as you had it before.
struct ContentView: View {
let overlayCol = Color.pink
@State private var saved = false
var body: some View {
somethingToBookmark
.overlay(alignment: .topTrailing) {
GlassEffectContainer {
if saved {
Button("Unsave", systemImage: "bookmark.fill") {
withAnimation { saved = false }
}
.buttonStyle(.glass(.clear.tint(overlayCol)))
} else {
Button("Save", systemImage: "bookmark") {
withAnimation { saved = true }
}
.buttonStyle(.glass(.clear.tint(overlayCol.opacity(0.4))))
}
}
.foregroundStyle(.white)
.labelStyle(.iconOnly)
.buttonBorderShape(.circle)
.offset(x: 10, y: -10)
}
}
private var somethingToBookmark: some View {
Text("Something to bookmark")
.font(.title3)
.fontWeight(.bold)
.padding(10)
.background(.white.opacity(0.5), in: .rect(cornerRadius: 10))
.padding(20)
.background(.image(Image(systemName: "xmark")).opacity(0.3))
.background(.gray.opacity(0.1), in: .rect(cornerRadius: 16))
}
}

EDIT You mentioned in a comment that:
the animation from pressing the button conflicts with the switch animation happening in the glass container
There are certainly two animations happening, which don’t get merged:
- The button grows and shrinks in size, depending on how long you press and hold. This is the interactive glass effect that comes with a glass button.
- The glass container provides a bouncy animation as one button is replaced by another.
The button animation still suffers from the problem you reported in the question, which is that it changes to its smaller size without animation when held and then released.
You are using the button to toggle the state of a boolean flag. For this purpose, the animation that you get with the glass container is perhaps sufficient (and imho, quite satisfactory) by itself, the button animation is not needed. So you might like to consider suppressing the interactive button effect altogether.
Unfortunately, it doesn’t work to add the modifier .interactive(false) to the glass supplied as parameter to glass button style:
.buttonStyle(.glass(
.clear
.tint(overlayCol)
.interactive(false) // 🙁 Doesn't work
))
However, you can disable the interactive glass effect by applying the .glassEffect to the button label yourself (as you were originally doing) and not applying .interactive() to the glass. Here’s how the example can be updated to work this way:
GlassEffectContainer {
if saved {
Button {
withAnimation { saved = false }
} label: {
Image(systemName: "bookmark.fill")
.resizable()
.scaledToFit()
.frame(width: 18, height: 18)
.padding(8)
}
.glassEffect(.clear.tint(overlayCol), in: .circle)
} else {
Button {
withAnimation { saved = true }
} label: {
Image(systemName: "bookmark")
.resizable()
.scaledToFit()
.frame(width: 18, height: 18)
.padding(8)
}
.glassEffect(.clear.tint(overlayCol.opacity(0.4)), in: .circle)
}
}
As far as addressing the question is concerned:
How to conditionally set tint color on interactive glass in SwiftUI iOS 26?
This approach stills gives an interactive glass effect, but the effect is coming from the GlassEffectContainer, not from the Button.
