Skip to content

Instantly share code, notes, and snippets.

@DevAndArtist
Created July 7, 2020 06:16
Show Gist options
  • Select an option

  • Save DevAndArtist/e50ddb6cc157d563786657bf30a411f9 to your computer and use it in GitHub Desktop.

Select an option

Save DevAndArtist/e50ddb6cc157d563786657bf30a411f9 to your computer and use it in GitHub Desktop.

Revisions

  1. DevAndArtist created this gist Jul 7, 2020.
    124 changes: 124 additions & 0 deletions swiftui_picker_attempt.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,124 @@
    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()
    }
    }