iOS / Programming / Swift

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

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です