feat(android): implement snackbar overlay behavior
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user