|
public enum BannerEdge { |
|
case top, bottom |
|
} |
|
public enum BannerAutoDismiss { |
|
case after(TimeInterval) |
|
case never |
|
} |
|
|
|
extension View { |
|
public func banner<C: View>( |
|
isPresented: Binding<Bool>, |
|
edge: BannerEdge = .top, |
|
autoDismiss: BannerAutoDismiss = .never, |
|
dragToDismiss: Bool = true, |
|
animation: Animation = .default, |
|
@ViewBuilder content: @escaping () -> C |
|
) -> some View { |
|
modifier(BannerModifier( |
|
isPresented: isPresented, edge: edge, autoDismiss: autoDismiss, dragToDismiss: dragToDismiss, animation: animation, banner: content |
|
)) |
|
} |
|
public func banner<T: Identifiable, C: View>( |
|
item: Binding<T?>, |
|
edge: BannerEdge = .top, |
|
autoDismiss: BannerAutoDismiss = .never, |
|
dragToDismiss: Bool = true, |
|
animation: Animation = .default, |
|
@ViewBuilder content: @escaping (T) -> C |
|
) -> some View { |
|
banner(isPresented: item.isActive(), edge: edge, autoDismiss: autoDismiss, dragToDismiss: dragToDismiss, animation: animation) { |
|
if let value = item.wrappedValue { |
|
content(value) |
|
} |
|
} |
|
} |
|
} |
|
|
|
private struct BannerModifier<C: View>: ViewModifier { |
|
@Binding var isPresented: Bool |
|
var id: AnyHashable = .init(UUID()) |
|
var edge: BannerEdge |
|
var autoDismiss: BannerAutoDismiss |
|
var dragToDismiss: Bool |
|
var animation: Animation |
|
var banner: () -> C |
|
|
|
@GestureState(resetTransaction: Transaction(animation: .easeInOut(duration: 0.3))) |
|
private var dragOffset = CGSize.zero |
|
|
|
private var dragGesture: some Gesture { |
|
return DragGesture() |
|
.updating($dragOffset, body: { (value, state, transaction) in |
|
guard dragToDismiss else { return } |
|
|
|
let play: CGFloat = 80 |
|
|
|
var movement = value.translation.height |
|
switch edge { |
|
case .top: movement = min(play, value.translation.height) |
|
case .bottom: movement = max(-play, value.translation.height) |
|
} |
|
|
|
state = CGSize(width: value.translation.width, height: movement) |
|
}) |
|
.onEnded { value in |
|
guard dragToDismiss else { return } |
|
|
|
switch edge { |
|
case .top where value.translation.height <= -35: |
|
isPresented = false |
|
case .bottom where value.translation.height >= 35: |
|
isPresented = false |
|
default: |
|
break |
|
} |
|
} |
|
} |
|
|
|
func body(content: Content) -> some View { |
|
let transistion: AnyTransition |
|
let alignment: Alignment |
|
|
|
switch edge { |
|
case .top: |
|
transistion = .move(edge: .top) |
|
alignment = .top |
|
case .bottom: |
|
transistion = .move(edge: .bottom) |
|
alignment = .bottom |
|
} |
|
|
|
return content |
|
.overlay( |
|
Group { |
|
if isPresented { |
|
banner() |
|
.frame(alignment: alignment) |
|
.offset(x: 0, y: dragOffset.height) |
|
.simultaneousGesture(dragGesture) |
|
.transition(transistion.combined(with: .opacity)) |
|
.task(id: id) { |
|
switch autoDismiss { |
|
case .after(let delay): |
|
do { |
|
try await Task.sleep(until: .now.advanced(by: .seconds(delay)), clock: .continuous) |
|
isPresented = false |
|
} catch { } |
|
|
|
case .never: |
|
break |
|
} |
|
} |
|
} |
|
}, |
|
alignment: alignment |
|
) |
|
.animation(animation, value: isPresented) |
|
} |
|
} |
Thanks for such a great snippet 🙌
But
.now.advanced
is only supported from iOS 16+. I have modified it a bit for lower iOS version.