SwiftUI でトースト・バナーを表示する方法
実装するもの
トースト・バナーとは?
トーストとは、ユーザーの操作に対して完了した後に表示する UI コンポーネントのことです。
主に画面下部に表示され、ファイルのダウンロードが完了したときや送信完了したときに使います。
バナーもトーストと似たような使われ方をしますが、主に画面上部に表示されます。iOS だと、通知バナーなどがあります。
カスタム Modifier を定義する
トースト・バナーは、コンテンツの上に何かしらの View を表示しています。この処理を共通化するために ViewModifier を使います。
詳しい ViewModifier の説明は、公式ドキュメントに譲りますが、簡単に説明すると複数の modifier を一つにして新しい modifier を作るためのプロトコルです。
今回は、 Overlay
という名前の Modifier を定義します。
// Overlay
struct Overlay<T: View>: ViewModifier {
@Binding var isShown: Bool
let overlayView: T
func body(contentView: Content) -> some View {
ZStack {
contentView
if isShown {
overlayView
}
}
}
}
contentView
が Overlay
の下に表示する View、 overlayView
がこれから実装する、トーストやバナーが渡されます。
isShown
の値が変わることで、 contentView
の上に overlayView
が表示されたり消えたりします。
トーストの実装方法
まず、最初にトーストの実装を紹介します。長くなってしまうので、細かい部分は消しているので、このままコピペしても動作はしないです。
struct Toast: View {
let data: DataModel
@Binding var isShown: Bool
var body: some View {
VStack {
Spacer()
HStack {
Image(systemName: data.image)
Text(data.title)
}
.font(.headline)
.foregroundColor(.primary)
.padding(20)
.background(Color(UIColor.secondarySystemBackground))
.clipShape(Capsule())
}
.frame(width: UIScreen.main.bounds.width / 1.25)
.transition(AnyTransition.move(edge: .bottom).combined(with: .opacity))
.onTapGesture {
withAnimation {
self.isShown = false
}
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
self.isShown = false
}
}
}
}
}
VStack
の一番上で Spacer
を置くことで下寄せになり、 transition
で下から出てくるようになっている。
また、時間経過 or 触ったら消えるように asyncAfter
と onTapGesture
を実装している。
トースト自体のデザインは VStack
内で実装しています。
バナーの実装方法
struct Banner: View {
// DatModel を定義
let data: DataModel?
@Binding var isShown: Bool
var body: some View {
if let data = data {
VStack {
HStack {
Image(systemName: data.type.sfSymbol)
VStack {
Text(data.title).bold()
if data.detail != "" {
Text(data.detail)
}
}
Spacer()
}
.foregroundColor(Color.white)
.padding(12)
.background(data.type.tintColor)
.cornerRadius(8)
Spacer()
}
.padding()
.animation(.easeInOut)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
self.isShown = false
}
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
self.isShown = false
}
}
}
}
}
}
トーストと違ってバナーは上部に出したいので、 VStack
内の一番下に Spacer
を配置します。また、 transition
も .bottom
ではなく .top
を指定します。
他の部分はほとんど Toast
に似ていて、バナーを消す処理やバナーのスタイルは自由に決められます。
使い方
今までに紹介した Overlay
と Toast
or Banner
を組み合わせることで、最初に紹介した UI が実装できます。
実際の使い方になります。
// View
VStack(spacing: 12) {
Button(action: { isShownBanner = true }) {
Text("バナーを表示する")
}
Button(action: { isShownToast = true }) {
Text("トーストを表示する")
}
}
.modifier(Overlay(isShown: $isShownBanner, overlayView: Banner(data: Banner.DataModel.success, isShown: $isShownBanner)))
.modifier(Overlay(isShown: $isShownToast, overlayView: Toast(data: Toast.DataModel.fileDownload, isShown: $isShownToast)))
Discussion
DispatchQueueじゃなくてwithAnimationのパラメータでdelay使って遅延した方がスマートそうです!