DEV Community

ShoheOhtani
ShoheOhtani

Posted on

[SwiftUI] How to create Toast View

Image description

How to use

struct ContentView: View {
    @State var presentTop: Bool = false

    var body: some View {
        VStack {
            TPButton(title: "☝️", action: {
                presentTop = true
            })
        }
        .navigationBarTitleDisplayMode(.inline) // if it's needed
        .toast(isPresented: $presentTop, edge: .top) { toastContent }
    }

    var toastContent: some View {
        HStack {
            Image(systemName: "checkmark.circle").font(.headline)
            Text("Toast!").font(.headline)
        }
        .frame(maxHeight: .infinity)
        .padding()
        .background(RoundedRectangle(cornerRadius: 8.0).foregroundColor(.random)) // set color whatever you want
    }
Enter fullscreen mode Exit fullscreen mode

3 Steps

  1. Create ToastView
  2. Create ViewModifier
  3. Create ViewModifier

Step1: Create ToastView

struct Toast<Content:View>: View {
    @Binding var isPresented: Bool
    @State var edge: Edge
    var content: Content
    let action: (() -> Void)?

    private var alignment: Alignment {
        switch edge {
        case .top: return .top
        case .leading: return .leading
        case .bottom: return .bottom
        case .trailing: return .trailing
        }
    }

    init(isPresented: Binding<Bool>, edge: Edge, action: (() -> Void)? = nil, @ViewBuilder content: () -> Content) {
        _isPresented = isPresented
        _edge = State(wrappedValue: edge)
        self.action = action
        self.content = content()
    }

    var body: some View {
        ZStack(alignment: alignment) {
            Spacer().frame(maxWidth: .infinity, maxHeight: .infinity)
            content
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .padding()
        .animation(.spring(), value: isPresented)
        .transition(.move(edge: edge).combined(with: .opacity))
        .onTapGesture {
            withAnimation {
                isPresented = false
                action?()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step2: Create ViewModifier

struct ToastModifier<ToastContent: View>: ViewModifier {

    @Binding var isPresented: Bool
    var toast: Toast<ToastContent>

    init(isPresented: Binding<Bool>, edge: Edge, action: (() -> Void)? = nil, @ViewBuilder toastContent: () -> ToastContent) {
        _isPresented = isPresented
        toast = Toast(isPresented: isPresented, edge: edge, action: action, content: toastContent)
    }

    public func body(content: Content) -> some View {
        ZStack {
            content
            if isPresented { toast }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.clear.ignoresSafeArea(.all, edges: .all))
        .animation(.spring(), value: isPresented)
    }
}
Enter fullscreen mode Exit fullscreen mode

Step3: View Extension

public extension View {
    func toast<Content:View>(isPresented: Binding<Bool>, edge: Edge, action: (() -> Void)? = nil, @ViewBuilder content: () -> Content) -> some View {
        self.modifier(ToastModifier(isPresented: isPresented, edge: edge, action: action, toastContent: content))
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
shohe profile image
ShoheOhtani •

ToastModifier has animation problem. When isPresented became false and ToastView will be hidden, it'll be hidden without fade-out animation.

Solution's here. 👇

public func body(content: Content) -> some View {
    content.overlay( // ZStack should be in .overlay()
        ZStack {
            if isPresented { toast }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.clear.ignoresSafeArea(.all, edges: .all))
        .animation(.spring(), value: isPresented)
    )
}
Enter fullscreen mode Exit fullscreen mode