0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftUIでMaterialDesignライクSnackbar作ってみた

0
Last updated at Posted at 2025-08-23

そもそも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は「公式の仕様です」を言いやすいのかな?

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?