Created
July 7, 2020 06:16
-
-
Save DevAndArtist/e50ddb6cc157d563786657bf30a411f9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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