I created a banner modifier that displays a banner from the top. This animates well. However, when I tap to dismiss it, it does not animate at all, just hides even though the tap gesture action has withAnimation
wrapping it.
struct BannerModifier: ViewModifier {
@Binding var model: BannerData?
func body(content: Content) -> some View {
content.overlay(
Group {
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "exclamationmark.triangle.fill")
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(.red)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.animation(.easeInOut)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
)
}
}
struct BannerData: Identifiable {
let id = UUID()
let title: String
let message: String?
}
In the tap gesture, I wipe out the model but it does not animate. It only hides immediately. How can I animate it so it slide up which is opposite of how it slide down to display? It would be really nice if I can also make the drag gesture interactive so I can slide it out like the native notifications.
SwiftUI has built-in support for animations with its animation() modifier. To use this modifier, place it after any other modifiers for your views, tell it what kind of animation you want, and also make sure you attach it to a particular value so the animation triggers only when that specific value changes.
SwiftUI uses Core Animation for rendering by default, and its performance is great for most animations. But if you find yourself creating a very complex animation that seems to suffer from lower framerates, you may want to utilize the power of Metal, which is Apple's framework used for working directly with the GPU.
By default, SwiftUI uses a fade animation to insert or remove views, but you can change that if you want by attaching a transition() modifier to a view.
Removing view from hierarchy is always animated by container, so to fix your modifier it is needed to apply .animation
on some helper container (note: Group
is not actually a real container).
Here is corrected variant
struct BannerModifier: ViewModifier {
@Binding var model: BannerData?
func body(content: Content) -> some View {
content.overlay(
VStack { // << holder container !!
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "exclamationmark.triangle.fill")
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.red)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
.animation(.easeInOut) // << here !!
)
}
}
Tested with Xcode 12.1 / iOS 14.1 and test view:
struct TestBannerModifier: View {
@State var model: BannerData?
var body: some View {
VStack {
Button("Test") { model = BannerData(title: "Error", message: "Fix It!")}
Button("Reset") { model = nil }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.modifier(BannerModifier(model: $model))
}
}
This is a slightly improoved version of the banner posted by Asperi Hope it helps someone.
import SwiftUI
public class BannerData {
public enum BannerType {
case warning, error, success
var textColor: Color {
switch self {
case .warning:
return .black
case .error:
return .white
case .success:
return .white
}
}
var backgroundColor: Color {
switch self {
case .warning:
return .yellow
case .error:
return .red
case .success:
return .green
}
}
var icon: String {
switch self {
case .warning:
return "exclamationmark.triangle.fill"
case .error:
return "exclamationmark.circle.fill"
case .success:
return "checkmark.circle.fill"
}
}
}
var type: BannerType = .success
let title: String
let message: String?
public init(title: String, message: String? = nil, type: BannerType) {
self.title = title
self.message = message
self.type = type
}
}
public struct BannerModifier: ViewModifier {
@Binding var model: BannerData?
public init(model: Binding<BannerData?>) {
_model = model
}
public func body(content: Content) -> some View {
content.overlay(
VStack {
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: model?.type.icon ?? BannerData.BannerType.success.icon)
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
Spacer()
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(model?.type.backgroundColor ?? .clear)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
.animation(.spring())
)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With