-
Notifications
You must be signed in to change notification settings - Fork 19
/
LazyPager.swift
233 lines (184 loc) · 7.38 KB
/
LazyPager.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//
// LazyPager.swift
// LazyPager
//
// Created by Brian Floersch on 7/6/23.
//
import Foundation
import UIKit
import SwiftUI
public enum LoadMore {
case lastElement(minus: Int = 0)
}
public enum DoubleTap {
case disabled
case scale(CGFloat)
}
public enum Direction {
case horizontal
case vertical
}
public enum ListPosition {
case beginning
case end
}
public enum ZoomConfig {
case disabled
case custom(min: CGFloat, max: CGFloat, doubleTap: DoubleTap)
}
public struct Config<Element> {
/// binding variable to control a custom background opacity. LazyPager is transparent by default
public var backgroundOpacity: Binding<CGFloat>?
/// Called when the view is done dismissing - dismiss gesture is disabled if nil
public var dismissCallback: (() -> ())?
/// Called when tapping once
public var tapCallback: (() -> ())?
/// Called when tapping twice
public var doubleTapCallback: (() -> ())?
/// The offset used to trigger load loadMoreCallback
public var loadMoreOn: LoadMore = .lastElement(minus: 3)
/// Called when more content should be loaded
public var loadMoreCallback: (() -> ())?
/// Direction of the pager
public var direction : Direction = .horizontal
/// Called whent the end of data is reached and the user tries to swipe again
public var overscrollCallback: ((ListPosition) -> ())?
/// The element index + the offset while paging
public var absoluteContentPosition: Binding<CGFloat>?
/// Called every view update to get the zoom config
public var zoomConfigGetter: (Element) -> ZoomConfig = { _ in .disabled }
/// Called while zooming to provide the current zoom level for an element
public var onZoomHandler: ((Element, CGFloat) -> ())?
/// Advanced settings (only accessibleevia .settings)
/// How may out of view pages to load in advance (forward and backwards)
public var preloadAmount: Int = 3
/// Minimum swipe velocity needed to trigger a dismiss
public var dismissVelocity: CGFloat = 1.3
/// the minimum % (between 0 and 1) you need to drag to trigger a dismiss
public var dismissTriggerOffset: CGFloat = 0.1
/// How long to animate the dismiss once done dragging
public var dismissAnimationLength: CGFloat = 0.2
/// Cancel SwiftUI animations. Default to true because the dismiss gesture is already animated.
/// Stacking animations can cause undesirable behavior
public var shouldCancelSwiftUIAnimationsOnDismiss = true
/// At what drag % (between 0 and 1) the background should be fully transparent
public var fullFadeOnDragAt: CGFloat = 0.2
/// The minimum scroll distance the in which the pinch gesture is enabled
public var pinchGestureEnableOffset: Double = 10
/// % ammount (from 0-1) of overscroll needed to call overscrollCallback
public var overscrollThreshold: Double = 0.15
}
public struct LazyPager<Element, DataCollecton: RandomAccessCollection, Content: View> where DataCollecton.Index == Int, DataCollecton.Element == Element {
private var viewLoader: (Element) -> Content
private var data: DataCollecton
@State private var defaultPageInternal = 0
private var providedPage: Binding<Int>?
private var page: Binding<Int> {
providedPage ?? Binding(
get: { defaultPageInternal },
set: { defaultPageInternal = $0 }
)
}
var config = Config<Element>()
public init(data: DataCollecton,
page: Binding<Int>? = nil,
direction: Direction = .horizontal,
@ViewBuilder content: @escaping (Element) -> Content) {
self.data = data
self.providedPage = page
self.viewLoader = content
self.config.direction = direction
}
public class Coordinator: ViewDataProvider<Content, DataCollecton, Element> { }
}
public extension LazyPager {
func onDismiss(backgroundOpacity: Binding<CGFloat>? = nil, _ callback: @escaping () -> ()) -> LazyPager {
guard config.direction == .horizontal else {
return self
}
var this = self
this.config.backgroundOpacity = backgroundOpacity
this.config.dismissCallback = callback
return this
}
func onTap(_ callback: @escaping () -> ()) -> LazyPager {
var this = self
this.config.tapCallback = callback
return this
}
func onDoubleTap(_ callback: @escaping () -> ()) -> LazyPager {
var this = self
this.config.doubleTapCallback = callback
return this
}
func shouldLoadMore(on: LoadMore = .lastElement(minus: 3), _ callback: @escaping () -> ()) -> LazyPager {
var this = self
this.config.loadMoreOn = on
this.config.loadMoreCallback = callback
return this
}
func zoomable(min: CGFloat, max: CGFloat, doubleTapGesture: DoubleTap = .scale(0.5)) -> LazyPager {
var this = self
this.config.zoomConfigGetter = { _ in
return .custom(min: min, max: max, doubleTap: doubleTapGesture)
}
return this
}
func zoomable(onElement: @escaping (Element) -> ZoomConfig) -> LazyPager {
var this = self
this.config.zoomConfigGetter = onElement
return this
}
func settings(_ adjust: @escaping (inout Config<Element>) -> ()) -> LazyPager {
var this = self
adjust(&this.config)
return this
}
func overscroll(_ callback: @escaping (ListPosition) -> ()) -> LazyPager {
var this = self
this.config.overscrollCallback = callback
return this
}
func absoluteContentPosition(_ absoluteContentPosition: Binding<CGFloat>? = nil) -> LazyPager {
guard config.direction == .horizontal else {
return self
}
var this = self
this.config.absoluteContentPosition = absoluteContentPosition
return this
}
func onZoom(_ onZoomHandler: @escaping (Element, CGFloat) -> ()) -> LazyPager {
var this = self
this.config.onZoomHandler = onZoomHandler
return this
}
}
extension LazyPager: UIViewControllerRepresentable {
public func makeUIViewController(context: Context) -> Coordinator {
DispatchQueue.main.async {
context.coordinator.goToPage(page.wrappedValue, animated: false)
}
return context.coordinator
}
public func makeCoordinator() -> Coordinator {
return Coordinator(data: data,
page: page,
config: config,
viewLoader: viewLoader
)
}
public func updateUIViewController(_ uiViewController: Coordinator, context: Context) {
uiViewController.viewLoader = viewLoader
uiViewController.data = data
defer { uiViewController.reloadViews() }
if page.wrappedValue != uiViewController.pagerView.currentIndex {
// Index was explicitly updated
uiViewController.goToPage(page.wrappedValue, animated: context.transaction.animation != nil)
}
if page.wrappedValue >= data.count {
uiViewController.goToPage(data.count - 1, animated: false)
} else if page.wrappedValue < 0 {
uiViewController.goToPage(0, animated: false)
}
}
}