Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animate a view to slide up and hide on tap in SwiftUI

Tags:

ios

swiftui

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?
}

enter image description here

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.

like image 241
TruMan1 Avatar asked Jan 23 '21 06:01

TruMan1


People also ask

How do I make an animation in SwiftUI?

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.

Does SwiftUI use Core Animation?

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.

How do I delete a view in SwiftUI?

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.


2 Answers

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).

demo

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))
    }
}
like image 113
Asperi Avatar answered Oct 20 '22 19:10

Asperi


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())
        )
    }
}
like image 1
Matias Contreras Avatar answered Oct 20 '22 20:10

Matias Contreras