Swift Bits: SwiftUI - Animate Binding
Animate State change
Small Intro
After months of publishing articles, you eventually reach a barrier. On one side, there’s an interesting topic or challenge — and on the other, there’s the question of how much information you can disclose to make it a full post. Substack, after all, is geared toward longer pieces with animations, code snippets, and videos.
Post after post, you build trust with your audience and set an expectation level. No, I don’t mean suddenly switching to baking or gardening 😄. With limited tag options (and they’re barely used here), the only thing left is the title and subtitle. So why not create a category and set the expected content type in advance?
In these Swift Bits posts, I want to share small but useful tips discovered during development or while preparing articles. If you’re a beatboxer or any kind of musician in your non-coding hours (hmm, spending free time not coding? What nonsense!) — you’ll recognize “Bits” as those small sound pieces that make a whole track shine.
To make transitions and modifier property changes, we use the .animation modifier. However, in some situations, it’s hard to trigger since we can’t just write:
withAnimation {
variable.toggle()
}Let’s check a sample:
struct SwiftBits: View {
//Some mode
enum PickerMode {
case left, right
}
@State private var mode: PickerMode = .left
var body: some View {
VStack {
//Button to toggle the mode
Button {
withAnimation {
mode = (mode != .left) ? .left : .right
}
} label: {
Text(”Change mode”)
.font(.title)
}
if mode == .left {
Text(”Left”)
.transition(.opacity.combined(with:.scale))
} else {
Text(”Right”)
.transition(.opacity.combined(with:.scale))
}
}
}
}Everything looks good. We have an appearance transition combining opacity and scale.
But what if we want to toggle it using a Picker?
struct SwiftBits: View {
//Some mode
enum PickerMode {
case left, right
}
@State private var mode: PickerMode = .left
var body: some View {
VStack {
//Button to toggle the mode
Button {
withAnimation {
mode = (mode != .left) ? .left : .right
}
} label: {
Text(”Change mode”)
.font(.title)
}
//Picker to toggle the mode
Picker(”Picker”, selection: $mode) {
Text(”Left”).tag(PickerMode.left)
Text(”Right”).tag(PickerMode.right)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
if mode == .left {
Text(”Left”)
.transition(.opacity.combined(with:.scale))
} else {
Text(”Right”)
.transition(.opacity.combined(with:.scale))
}
}
}
}Unfortunately, this doesn’t work as expected. One option, is to write a custom Binding with get/set:
var body: some View {
let animatedModeBinding = Binding<PickerMode>(
get: { mode },
set: { newValue in
withAnimation {
mode = newValue
}
}
)This looks like too much for such simple logic, and there’s a more convenient solution!
Binding has a lot of extensions, and one of them will help us:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Binding {
/// Specifies a transaction for the binding.
///
/// - Parameter transaction : An instance of a ``Transaction``.
///
/// - Returns: A new binding.
public func transaction(_ transaction: Transaction) -> Binding<Value>
/// Specifies an animation to perform when the binding value changes.
///
/// - Parameter animation: An animation sequence performed when the binding
/// value changes.
///
/// - Returns: A new binding.
public func animation(_ animation: Animation? = .default) -> Binding<Value>
}.animation will return new Binding with animation attached.
struct SwiftBits: View {
//Some mode
enum PickerMode {
case left, right
}
@State private var mode: PickerMode = .left
var body: some View {
VStack {
//Button to toggle the mode
Button {
withAnimation {
mode = (mode != .left) ? .left : .right
}
} label: {
Text(”Change mode”)
.font(.title)
}
//Picker to toggle the mode
Picker(”Picker”, selection: $mode.animation()) {
Text(”Left”).tag(PickerMode.left)
Text(”Right”).tag(PickerMode.right)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
if mode == .left {
Text(”Left”)
.transition(.opacity.combined(with:.scale))
} else {
Text(”Right”)
.transition(.opacity.combined(with:.scale))
}
}
}
}Working like a charm! Gist is here.

