Skip to content

Instantly share code, notes, and snippets.

@sameersyd
Last active September 18, 2023 06:41
Show Gist options
  • Save sameersyd/fce9599687963fca90677d959dce7a6e to your computer and use it in GitHub Desktop.
Save sameersyd/fce9599687963fca90677d959dce7a6e to your computer and use it in GitHub Desktop.
SwiftUI - Two Directional SnapList
// Checkout the explanation article here - https://sameer-syd.medium.com/swiftui-two-directional-snaplist-95cb852957be
import SwiftUI
import Combine
struct HomeView: View {
@StateObject var viewModel: HomeViewModel
var body: some View {
GeometryReader { geo in
ZStack {
ScrollViewReader { reader in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(0..<(viewModel.messages.count), id: \.self) { i in
TabViewCell(media: viewModel.messages[i].media)
.frame(width: geo.size.width, height: geo.size.height)
.id(i)
}
}
.background(GeometryReader {
Color.clear.preference(key: ScrollOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ScrollOffsetKey.self) { viewModel.scrollDetector.send($0) }
}
.coordinateSpace(name: "scroll")
.onReceive(viewModel.scrollPublisher) {
var index = $0/geo.size.height
// Check if next item is near
let value = index < 1 ? index : index.truncatingRemainder(dividingBy: CGFloat(Int(index)))
if value > 0.5 { index += 1 }
else { index = CGFloat(Int(index)) }
// Scroll to index
withAnimation { reader.scrollTo(Int(index), anchor: .center) }
}
}
}.edgesIgnoringSafeArea(.all)
}.edgesIgnoringSafeArea(.all)
}
}
// To Get Scroll Offset
fileprivate struct ScrollOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
// -----------------------------------------
class HomeViewModel: ObservableObject {
let messages: [Message]
let scrollDetector: CurrentValueSubject<CGFloat, Never>
let scrollPublisher: AnyPublisher<CGFloat, Never>
init(messages: [Message]) {
self.messages = messages
// detect when scroll ends
let detector = CurrentValueSubject<CGFloat, Never>(0)
self.scrollPublisher = detector
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.dropFirst().eraseToAnyPublisher()
self.scrollDetector = detector
}
}
@marcjjbuchser
Copy link

marcjjbuchser commented Apr 26, 2023

The TabViewCell & Message are missing, do you have a working example?

@sameersyd
Copy link
Author

sameersyd commented May 16, 2023

@marcjjbuchser Consider this ProfileView to be the TabViewCell

struct ProfileView: View {

    let images: [String]
    @State private var selection = 0

    var body: some View {
        GeometryReader { geo in
            ZStack {
                TabView(selection: $selection) {
                    ForEach(0..<(images.count), id: \.self) { i in
                        KFImage(URL(string: images[i]))
                            .resizable()
                            .scaledToFill()
                            .frame(width: geo.size.width, height: geo.size.height)
                            .clipped()
                    }
                }.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            }.edgesIgnoringSafeArea(.all).clipped()
        }.edgesIgnoringSafeArea(.all)
    }
}

@marcjjbuchser
Copy link

Thanks.
Here is an updated example with a TabImageView (TabViewCell), Media and Message class and usage example:

https://github.com/marcjjbuchser/TwoDirectionalScrollView

The complete xcode project is coming soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment