ListlessiOS/Views/PullToClear.swift (1799B)
1import SwiftUI
2
3
4let pullClearThreshold: CGFloat = 50
5
6struct PullToClearIndicator: View {
7 let pullOffset: CGFloat
8
9 private var progress: CGFloat { min(1, pullOffset / pullClearThreshold) }
10 private var isReady: Bool { pullOffset >= pullClearThreshold }
11 private let textSlideDistance: CGFloat = 22
12
13 var body: some View {
14 HStack(spacing: 6) {
15 ZStack {
16 Image(systemName: "checkmark")
17 .offset(y: isReady ? 0 : textSlideDistance)
18 Image(systemName: "tray")
19 .offset(y: isReady ? -textSlideDistance : 0)
20 }
21 .frame(width: 26, height: textSlideDistance, alignment: .leading)
22 .clipped()
23 .foregroundStyle(.secondary)
24 .font(.system(size: 17))
25 .fontWeight(.semibold)
26 .animation(.easeInOut(duration: 0.15), value: isReady)
27 ZStack(alignment: .leading) {
28 Text("Release to clear")
29 .offset(y: isReady ? 0 : textSlideDistance)
30 Text("Clear completed")
31 .offset(y: isReady ? -textSlideDistance : 0)
32 }
33 .foregroundStyle(.secondary)
34 .font(ItemRowMetrics.hintSUI)
35 .frame(height: textSlideDistance, alignment: .topLeading)
36 .clipped()
37 .animation(.easeInOut(duration: 0.15), value: isReady)
38 }
39 .frame(maxWidth: .infinity)
40 .frame(height: 56)
41
42 .frame(height: min(pullOffset, 56), alignment: .bottom)
43 .clipped()
44 .opacity(Double(progress))
45 .allowsHitTesting(false)
46 }
47}