Search code examples
iosswiftswiftuiuikittoast

How to show a message balloon in SwiftUI?


This component usually shows when you connect to Airpods. That shows a message in a toast/balloon/pill shape at the top of the screen, with a small image (icon) and a title, it would dismiss in a few seconds, similar to the toast on the Android side.

Is this a built-in view control? Check my screenshot for more details.

Screenshot1


Solution

  • Not built-in but easy to make

    import SwiftUI
    struct ToastParentView: View {
        @State var showToast: Bool = false
        var body: some View {
            VStack{
                Button(action: {
                    showToast.toggle()
                }, label: {
                    Text("show toast")
                })
                List(){
                    ForEach(0...20, id: \.self, content: { idx in
                        Text("\(idx)")
                    })
                }
            }.toast(showToast: $showToast, position: .top, toastContent: {
                VStack{
                    Text("Hello World!!")
                    Text("Headline").font(.headline)
                }
            })
        }
    }
    extension View {
        func toast<T:View>(showToast: Binding<Bool>, duration: TimeInterval = 5, position: ToastView<T>.ToastPosition = .top, @ViewBuilder toastContent: @escaping () -> T) -> some View {
            modifier(ToastView(showToast: showToast, toastContent: toastContent(), duration: duration, position: position))
            
        }
    }
    struct ToastView<T: View>: ViewModifier {
        @Binding var showToast: Bool
        let toastContent: T
        @State var timer: Timer?
        let duration: TimeInterval
        let position: ToastPosition
        func body(content: Content) -> some View {
            GeometryReader{ geo in
                ZStack{
                    content
                    if showToast{
                        RoundedRectangle(cornerRadius: 25)
                            .foregroundColor(Color(UIColor.systemBackground))
                            .shadow(radius: 10)
                            .overlay(toastContent
                                        .minimumScaleFactor(0.2))
                            .frame(maxWidth: geo.size.width*0.6, maxHeight: 55 ,alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        
                            .position(x: geo.size.width/2, y: getYPosition(height: geo.size.height, safeAreaInset: geo.safeAreaInsets))
                            .onAppear(){
                                timer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false, block: {_ in
                                    showToast = false
                                })
                            }
                            .onDisappear(){
                                print("onDisappear")
                                timer?.invalidate()
                                timer = nil
                            }
                        
                    }
                }.onTapGesture {
                    showToast = false
                }
            }
            
        }
        enum ToastPosition{
            case top
            case bottom
            case middle
        }
        func getYPosition(height: CGFloat, safeAreaInset: EdgeInsets) -> CGFloat{
            var result: CGFloat = 0
            if position == .top{
                result = 0 + safeAreaInset.top
            }else if position == .bottom{
                result = height - safeAreaInset.bottom
            }else if position == .middle{
                result = height/2
            }
            return result
        }
    }