04
Feb
2022
SwiftUI Toast
Reading time: 2 min
This recipe shows how to implement an Android-like "toast" notification in SwiftUI.
The end result looks like this:
Features include:
- Configurable toast view with custom text, font, foreground and background color. It defaults to Android-style toast with white text on a semi-transparent black background.
- The toast is automatically dismissed after the given time frame or when it's tapped on.
- Showing and dissmissing of the toast is animated by default and can be configured with an animation and/or transition.
Here's the full code:
struct Toast: ViewModifier {
// these correspond to Android values f
// or DURATION_SHORT and DURATION_LONG
static let short: TimeInterval = 2
static let long: TimeInterval = 3.5
let message: String
@Binding var isShowing: Bool
let config: Config
func body(content: Content) -> some View {
ZStack {
content
toastView
}
}
private var toastView: some View {
VStack {
Spacer()
if isShowing {
Group {
Text(message)
.multilineTextAlignment(.center)
.foregroundColor(config.textColor)
.font(config.font)
.padding(8)
}
.background(config.backgroundColor)
.cornerRadius(8)
.onTapGesture {
isShowing = false
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + config.duration) {
isShowing = false
}
}
}
}
.padding(.horizontal, 16)
.padding(.bottom, 18)
.animation(config.animation, value: isShowing)
.transition(config.transition)
}
struct Config {
let textColor: Color
let font: Font
let backgroundColor: Color
let duration: TimeInterval
let transition: AnyTransition
let animation: Animation
init(textColor: Color = .white,
font: Font = .system(size: 14),
backgroundColor: Color = .black.opacity(0.588),
duration: TimeInterval = Toast.short,
transition: AnyTransition = .opacity,
animation: Animation = .linear(duration: 0.3)) {
self.textColor = textColor
self.font = font
self.backgroundColor = backgroundColor
self.duration = duration
self.transition = transition
self.animation = animation
}
}
}
extension View {
func toast(message: String,
isShowing: Binding<Bool>,
config: Toast.Config) -> some View {
self.modifier(Toast(message: message,
isShowing: isShowing,
config: config))
}
func toast(message: String,
isShowing: Binding<Bool>,
duration: TimeInterval) -> some View {
self.modifier(Toast(message: message,
isShowing: isShowing,
config: .init(duration: duration)))
}
}
Then, just place the modifier where necessary and trigger it by setting the isShowing
binding:
struct ToastTest: View {
@State private var showToast = false
var body: some View {
NavigationView {
List(1..<100) { index in
Text("Row \(index)")
}
.toast(message: "Current time:\n\(Date().formatted(date: .complete, time: .complete))",
isShowing: $showToast,
duration: Toast.short)
.navigationBarTitle("Toast Testing")
.navigationBarItems(leading: Button("Show") {
showToast = true
})
}
}
}