Skip to content

Instantly share code, notes, and snippets.

@DevAndArtist
Created July 7, 2020 06:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DevAndArtist/e50ddb6cc157d563786657bf30a411f9 to your computer and use it in GitHub Desktop.
Save DevAndArtist/e50ddb6cc157d563786657bf30a411f9 to your computer and use it in GitHub Desktop.
import Combine
import SwiftUI
// To re-align the cells to the center we use a workaround through debouncing
// the nearest cell ID which is computed every time the scroll view moves.
//
// HOWEVER there is a bug that still needs to be solved:
// If you drag the scroll view and hold, the debounce event will still happen
// and reposition the cell.
//
// To solve the issue we need `isTracking` state for the scroll view, which
// we'll use to filter out unwanted events.
struct PickerTest: View {
final class _Helper: ObservableObject {
let _subject: PassthroughSubject<Int, Never>
// This object should never update.
let objectWillChange = Empty<Never, Never>(completeImmediately: false)
var yInitial: CGFloat?
let idPublisher: AnyPublisher<Int, Never>
init() {
let subject = PassthroughSubject<Int, Never>()
self._subject = subject
self.idPublisher = subject
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
func scrollTo(id: Int) {
_subject.send(id)
}
}
// Make sure that `_Helper` object is instantiated only once,
// and its instance is reused during every `body` call.
@StateObject
var _helper = _Helper()
func action(with proxy: ScrollViewProxy, id: Int) -> () -> Void {
return {
withAnimation {
proxy.scrollTo(id, anchor: UnitPoint(x: 0.5, y: 0.5))
}
}
}
var body: some View {
ScrollViewReader { proxy in
HStack {
ScrollView
.init {
VStack(spacing: 0) {
// top inset
GeometryReader
.init { proxy -> Color in
// compute y offset
let yGlobal = proxy.frame(in: .global).origin.y
let yInitial = _helper.yInitial ?? yGlobal
_helper.yInitial = yInitial
let yOffset = yInitial - yGlobal
// compute closest id and clamp it
let id = Int((yOffset / 40).rounded())
let clampedID = min(40, max(0, id))
// forward the id to the debouncing publisher
_helper.scrollTo(id: clampedID)
// return a transparent view
return Color.clear
}
.frame(width: 0, height: 120)
// items
LazyVStack(spacing: 0) {
ForEach
.init(0 ... 40, id: \.self) { id in
Button {
withAnimation {
proxy.scrollTo(id, anchor: UnitPoint(x: 0.5, y: 0.5))
}
} label: {
Text("\(id)")
.frame(width: 100, height: 40)
.background(Color.green)
.border(Color.blue, width: 1)
}
}
.border(Color.red, width: 1)
}
// bottom inset
Color
.clear
.frame(width: 0, height: 120)
}
}
.frame(width: 200, height: 280)
.border(Color.black, width: 1)
.background(
Color
.orange
.frame(width: 200, height: 40)
)
.onReceive(_helper.idPublisher) { id in
withAnimation {
proxy.scrollTo(id, anchor: UnitPoint(x: 0.5, y: 0.5))
}
}
VStack {
Button("scroll to 0", action: action(with: proxy, id: 0))
Button("scroll to 40", action: action(with: proxy, id: 40))
}
}
}
}
}
struct PickerTest_Previews: PreviewProvider {
static var previews: some View {
PickerTest()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment