diff --git a/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift b/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift index 98a0bde..379b084 100644 --- a/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift +++ b/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift @@ -3,13 +3,51 @@ import SwiftUI struct SnackbarOverlay: View { #if os(Android) let center: SnackbarCenter + @State var current: SnackbarMessage? + @State var observer: Any? #else @ObservedObject var center: SnackbarCenter #endif var body: some View { #if os(Android) - EmptyView() + VStack { + if let message = current { + HStack(spacing: 10) { + if let icon = message.iconSystemName, !icon.isEmpty { + Image(systemName: icon) + .font(.system(size: 16, weight: .semibold)) + } + Text(message.title) + .font(AppTypography.heading3) + .multilineTextAlignment(.leading) + .lineLimit(3) + Spacer(minLength: 0) + } + .foregroundStyle(Color.white) + .padding(.top, 14) + .padding(.horizontal, 16) + .padding(.bottom, 16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(message.style.backgroundColor) + .appContentShape(Rectangle()) + .onTapGesture { + center.handleTap() + } + .transition(.move(edge: .top).combined(with: .opacity)) + .zIndex(999) + } + Spacer() + } + .animation(.spring(response: 0.3, dampingFraction: 0.9), value: current?.id) + .allowsHitTesting(current != nil) + .onAppear { + current = center.current + attachObserverIfNeeded() + } + .onDisappear { + detachObserver() + } #else VStack { if let message = center.current { @@ -43,4 +81,25 @@ struct SnackbarOverlay: View { .allowsHitTesting(center.current != nil) #endif } + +#if os(Android) + private func attachObserverIfNeeded() { + guard observer == nil else { return } + observer = NotificationCenter.default.addObserver( + forName: .snackbarDidChange, + object: nil, + queue: nil + ) { _ in + Task { @MainActor in + current = center.current + } + } + } + + private func detachObserver() { + guard let observer else { return } + NotificationCenter.default.removeObserver(observer) + self.observer = nil + } +#endif } diff --git a/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift b/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift index 093ed8e..d9f959d 100644 --- a/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift +++ b/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift @@ -1,11 +1,16 @@ import Foundation import SwiftUI +extension Notification.Name { + static let snackbarDidChange = Notification.Name("snackbarDidChange") +} + #if os(Android) @MainActor final class SnackbarCenter { static let shared = SnackbarCenter() var current: SnackbarMessage? + private var dismissTask: Task? func show( title: String, @@ -14,11 +19,40 @@ final class SnackbarCenter { duration: TimeInterval = 3.5, action: (() -> Void)? = nil ) { + dismissTask?.cancel() + current = SnackbarMessage( + title: title, + style: style, + iconSystemName: icon, + duration: duration, + action: action + ) + notifyChange() + + dismissTask = Task { [weak self] in + let nanos = UInt64(max(0.2, duration) * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanos) + guard !Task.isCancelled else { return } + self?.dismiss(animated: true) + } + } + + func handleTap() { + let action = current?.action + dismiss(animated: true) action?() } - func handleTap() {} - func dismiss(animated: Bool) {} + func dismiss(animated: Bool) { + dismissTask?.cancel() + dismissTask = nil + current = nil + notifyChange() + } + + private func notifyChange() { + NotificationCenter.default.post(name: .snackbarDidChange, object: nil) + } } #else @MainActor diff --git a/skip_for_android_plan.md b/skip_for_android_plan.md index 3d08135..a62475a 100644 --- a/skip_for_android_plan.md +++ b/skip_for_android_plan.md @@ -297,7 +297,7 @@ Legenda: 10. 🔴 Validar perfil e endereços no Android. 11. 🔴 Validar permissão e uso de localização no Android. 12. 🔴 Corrigir bugs críticos de runtime Android. -13. 🔴 Ajustar snackbar Android. +13. 🟢 Ajustar snackbar Android. 14. 🔴 Revisar splash Android, se necessário. 15. 🔴 Rodar regressão rápida no iOS após os ajustes Android. 16. 🟢 Criar commit após cada task concluída e funcional. @@ -318,3 +318,8 @@ Legenda: 5. Task 5 concluída com geração validada de artefatos Android instaláveis. - APK exportado com sucesso. - AAB exportado com sucesso. +6. Task 13 concluída com implementação funcional de snackbar no Android. + - `SnackbarCenter` Android deixou de ser stub e agora controla exibição, timeout e tap action. + - `SnackbarOverlay` Android passou a renderizar o banner visual e reagir a mudanças de estado. + - `swift build` validado com sucesso após a alteração. + - A validação `skip export` desta rodada ficou bloqueada por erro pré-existente em `Vendor/skip-fuse-ui` (`onGeometryChangeErased`) e em código gerado `SkipUI`, sem relação direta com a task de snackbar.