Reputation: 768
I have a component called Alert. It displays an icon, title and description in a consistent way. To show the Alert, I have an optional binding (if the binding is not passed in, it defaults to true which always shows the alert).
I would like for the alert to always animate in/out. There is a close button inside the alert, and when I wrap it with
withAnimation(.easeInOut, {
self.isVisible?.wrappedValue.toggle()
})
the alert fades in/out and also nicely animates the surrounding elements.
The problem i have run into is, if the binding updates outside of the Alert (e.g. in the parent), then it does not animate. This makes sense as it is not wrapped with the withAnimation
method. If I wrap where it updates with withAnimation
then all works as expected. However, I want for the alert to control how it animates in/out, I don't want to rely on the place updating the binding to have to call withAnimation
.
var isVisible: Binding<Bool> = .constant(true)
var body: some View {
if self.isVisible.wrappedValue {
VStack {
// Alert component stuff here
}
// I found that if I add this modifier here, then the view will nicely animate/slide
// into the surrounding view, but does not fade in/out
.animation(.easeInOut)
}
}
What I am essentially looking for, is how do I have my alert fade in/out and nicely slide into the surrounding views (like withAnimation
does), but regardless of whether the binding is updated from the internal close button or an external parent.
UPDATE: Minimal code example - excuse some hacks i had to pull it out of the design system:
import SwiftUI
struct AlertStyles {
var icon: Image;
var color: Color;
var background: Color;
}
enum AlertType: CaseIterable {
case info
var style: AlertStyles {
switch self {
case .info:
return AlertStyles(
icon: Image(systemName: "info.circle"),
color: Color(red: 0.70, green: 0.49, blue: 0.00),
background: Color(red: 1.00, green: 0.97, blue: 0.90)
)
}
}
}
struct AlertBase: View {
/// The text to use as the title of the alert
var title: String;
/// The type of the alert
var type: AlertType = .info;
/// Whether or not the alert is displaying inline
var isInline: Bool = false;
var body: some View {
HStack(spacing: self.isInline ? CGFloat(8) : CGFloat(12)) {
self.type.style.icon
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.foregroundColor(self.type.style.color)
.frame(width: 20, height: 20)
Text(self.title)
.foregroundColor(Color(red: 0.09, green: 0.15, blue: 0.32))
.font(.system(size: 14, weight: .semibold))
.lineLimit(2)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct Alert: View {
/// The type of the alert
var type: AlertType = .info;
/// The text to use as the title of the alert
var title: String;
/// An optional detail descriptor
var description: String? = nil;
/// An optional binding for whether or not the alert is visible
/// **Note**: When binding is not present, the alert will
/// always show
var isVisible: Binding<Bool> = .constant(true)
/// Whether or not the alert can be closed via a displayed
/// close button
var isClosable: Bool = false;
var body: some View {
if self.isVisible.wrappedValue {
VStack(alignment: .leading, spacing: 4) {
HStack {
AlertBase(title: self.title, type: self.type)
if isClosable {
Button(
action: {
// HERE: The behaviur of withAnimation is desired
// withAnimation(.easeInOut, {
self.isVisible.wrappedValue.toggle()
// })
},
label: {
Image(systemName: "xmark")
.renderingMode(.template)
.foregroundColor(Color(red: 0.42, green: 0.45, blue: 0.56))
.frame(
width: 20,
height: 20
)
}
)
}
}
if let alertDescription = self.description {
Text(alertDescription)
.padding(.leading, 32)
.font(.system(size: 14))
.foregroundColor(Color(red: 0.27, green: 0.32, blue: 0.46))
}
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(self.type.style.background)
.cornerRadius(6)
}
}
}
struct Alert_InteractivePreview: View {
@State private var isVisible: Bool = true;
@State private var isVisible1: Bool = true;
var body: some View {
VStack(alignment: .center, spacing: 16) {
Alert(
type: .info,
title: "Alert type warn",
description: "This is the first description",
isVisible: self.$isVisible,
isClosable: true
);
Alert(
type: .info,
title: "Alert type warn",
description: "This is the second description",
isVisible: self.$isVisible1,
isClosable: true
);
Button(action: {
self.isVisible.toggle()
}, label: {
Text("Toggle Alert 1 Visibility")
})
Button(action: {
self.isVisible1.toggle()
}, label: {
Text("Toggle Alert 2 Visibility")
})
}
.padding()
}
}
struct Alert_Previews: PreviewProvider {
static var previews: some View {
Alert_InteractivePreview()
}
}
Upvotes: 2
Views: 713
Reputation: 257729
Here is fixed code that gives the same effect as using withAnimation
in button.
Tested with Xcode 12.5 / iOS 14.5
var body: some View {
VStack {
if self.isVisible.wrappedValue {
VStack(alignment: .leading, spacing: 4) {
// .. alert content here
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(self.type.style.background)
.cornerRadius(6)
}
}
.animation(.easeInOut)
}
Upvotes: 2