feat(android): implement snackbar overlay behavior

This commit is contained in:
Daniel Arantes Loverde
2026-04-17 14:39:29 -03:00
parent 0ebb854213
commit 9b00c472dc
3 changed files with 102 additions and 4 deletions

View File

@@ -3,13 +3,51 @@ import SwiftUI
struct SnackbarOverlay: View { struct SnackbarOverlay: View {
#if os(Android) #if os(Android)
let center: SnackbarCenter let center: SnackbarCenter
@State var current: SnackbarMessage?
@State var observer: Any?
#else #else
@ObservedObject var center: SnackbarCenter @ObservedObject var center: SnackbarCenter
#endif #endif
var body: some View { var body: some View {
#if os(Android) #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 #else
VStack { VStack {
if let message = center.current { if let message = center.current {
@@ -43,4 +81,25 @@ struct SnackbarOverlay: View {
.allowsHitTesting(center.current != nil) .allowsHitTesting(center.current != nil)
#endif #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
} }

View File

@@ -1,11 +1,16 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
extension Notification.Name {
static let snackbarDidChange = Notification.Name("snackbarDidChange")
}
#if os(Android) #if os(Android)
@MainActor @MainActor
final class SnackbarCenter { final class SnackbarCenter {
static let shared = SnackbarCenter() static let shared = SnackbarCenter()
var current: SnackbarMessage? var current: SnackbarMessage?
private var dismissTask: Task<Void, Never>?
func show( func show(
title: String, title: String,
@@ -14,11 +19,40 @@ final class SnackbarCenter {
duration: TimeInterval = 3.5, duration: TimeInterval = 3.5,
action: (() -> Void)? = nil 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?() 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 #else
@MainActor @MainActor

View File

@@ -297,7 +297,7 @@ Legenda:
10. 🔴 Validar perfil e endereços no Android. 10. 🔴 Validar perfil e endereços no Android.
11. 🔴 Validar permissão e uso de localização no Android. 11. 🔴 Validar permissão e uso de localização no Android.
12. 🔴 Corrigir bugs críticos de runtime Android. 12. 🔴 Corrigir bugs críticos de runtime Android.
13. 🔴 Ajustar snackbar Android. 13. 🟢 Ajustar snackbar Android.
14. 🔴 Revisar splash Android, se necessário. 14. 🔴 Revisar splash Android, se necessário.
15. 🔴 Rodar regressão rápida no iOS após os ajustes Android. 15. 🔴 Rodar regressão rápida no iOS após os ajustes Android.
16. 🟢 Criar commit após cada task concluída e funcional. 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. 5. Task 5 concluída com geração validada de artefatos Android instaláveis.
- APK exportado com sucesso. - APK exportado com sucesso.
- AAB 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.