Last active
September 18, 2023 06:41
-
-
Save sameersyd/fce9599687963fca90677d959dce7a6e to your computer and use it in GitHub Desktop.
SwiftUI - Two Directional SnapList
This file contains 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
// 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 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)
}
}
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
The TabViewCell & Message are missing, do you have a working example?