
SwiftUIでMaterialDesignライクSnackbar作ってみた
そもそもMaterial Designを使えばいいじゃんって話は置いておいて。。。
import SwiftUI
extension View {
func snackbar(
isReady: Binding<Bool>,
message: Binding<String>,
duration: Binding<Snackbar.Duration>
) -> some View {
self.overlay(alignment: .bottom) {
Snackbar(
isReady: isReady,
message: message,
duration: duration
)
}
}
}
struct Snackbar: View {
@Binding var isReady: Bool
@Binding var message: String
@Binding var duration: Duration
@State private var visible = false
var body: some View {
HStack {
Spacer().frame(width: 20)
GroupBox {
ZStack {
Text(message)
.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .leading
)
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.shadow(radius: 10)
Spacer().frame(width: 20)
}
.opacity(visible && isReady ? 1 : 0)
.onChange(of: message) {
hideSnackbar(
duration: $duration,
visible: $visible
)
}
.onChange(of: duration) {
hideSnackbar(
duration: $duration,
visible: $visible
)
}
}
}
private func hideSnackbar(
duration: Binding<Snackbar.Duration>,
visible: Binding<Bool>
) {
visible.wrappedValue = true
DispatchQueue.main.asyncAfter(
deadline:
.now() + DispatchTimeInterval
.seconds(duration.wrappedValue.rawValue)
) {
withAnimation(.easeInOut(duration: 0.2)) {
visible.wrappedValue = false
}
}
}
extension Snackbar {
enum Duration: Int, Equatable {
case long = 6
case short = 3
static func == (lhs: Duration, rhs: Duration) -> Bool {
switch (lhs, rhs) {
case (long, long):
return true
case (long, short):
return false
case (short, long):
return false
case (short, short):
return true
}
}
}
}
使い方はこんな感じ
#Preview {
@Previewable @State var isReady = false
@Previewable @State var message = ""
@Previewable @State var duration = Snackbar.Duration.long
var count = 0
GeometryReader { geometry in
Button(
action: {
isReady = true
count += 1
message = "Hello \(count)"
duration = if count % 2 == 0 {
Snackbar.Duration.short
} else {
Snackbar.Duration.long
}
},
label: {
Text("Click Here")
}
)
}
.snackbar(
isReady: $isReady,
message: $message,
duration: $duration
)
}
見た目はこんな感じ

注意と意図
Snackbarで受け取っているisReadyは、snackbarの呼び出し時はsnackbarを非表示にするために使用しています。
isReadyを実装する前は、snackbarを呼び出すだけでsnackbarが表示されていましたが、呼び出し時と実際の表示タイミングは異なるための引数です。
snackbarが表示される条件は下記のとおりです。
・isReadyがfalseからtrueに変更された時
・messageが変更された時
・durationが変更された時
今日の一言
やっぱりAndroidの方がdeveloperフレンドリーな気がする。。。
公式文章も見やすいし、例も充実してるし、オープンソースで追いかけやすいし
ただiOSは「公式の仕様です」を言いやすいのかな?