Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active March 1, 2024 21:49
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IanKeen/82be47f59b488cd535e176290c1690d5 to your computer and use it in GitHub Desktop.
Save IanKeen/82be47f59b488cd535e176290c1690d5 to your computer and use it in GitHub Desktop.
Persistent banner in SwiftUI : Supports top/bottom edge, swipe to dismiss + auto dismissal after time
struct MyView: View {
@State var showBanner = false
var body: some View {
NavigationView {
VStack(spacing: 18) {
Text("hello world")
Button("Toggle", action: showBanner.toggle)
NavigationLink("Push", destination: VStack {
Button("Toggle", action: showBanner.toggle)
})
}
.navigationTitle("Testing 1 2 3")
}
.banner(isPresented: $showBanner, autoDismiss: .after(2)) {
Color.red
.frame(height: 100, alignment: .center)
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
.padding([.leading, .trailing])
}
}
}
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)
}
}
@trong-codelink
Copy link

trong-codelink commented Mar 8, 2023

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.

// ...
                           case let .after(delay):
                                    do {
                                        let delayNanosec = UInt64(delay * 1_000_000_000)
                                        try await Task.sleep(nanoseconds: delayNanosec)
                                        isPresented = false
                                    } catch {}
// ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment