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