From 845bf7000098a67feac9fddcd7fec4d50f524b7c Mon Sep 17 00:00:00 2001 From: Trent Guillory Date: Fri, 19 Nov 2021 08:55:42 -0600 Subject: [PATCH] add callback for view layout --- .../Example 2/Example2ContentView.swift | 28 ++++++++--- .../Example3/Example3ContentView.swift | 25 +++++++--- Sources/HStackSnap.swift | 14 +++--- Sources/Model/SnapToScrollEvent.swift | 11 +++++ Sources/Views/HStackSnapCore.swift | 47 ++++++++----------- 5 files changed, 75 insertions(+), 50 deletions(-) create mode 100644 Sources/Model/SnapToScrollEvent.swift diff --git a/SnapToScrollDemo/Sources/Example 2/Example2ContentView.swift b/SnapToScrollDemo/Sources/Example 2/Example2ContentView.swift index 6005215..1d7f7c0 100644 --- a/SnapToScrollDemo/Sources/Example 2/Example2ContentView.swift +++ b/SnapToScrollDemo/Sources/Example 2/Example2ContentView.swift @@ -1,34 +1,48 @@ -import SwiftUI import SnapToScroll +import SwiftUI // MARK: - Example2ContentView struct Example2ContentView: View { + var body: some View { - VStack { - + Text("Explore Nearby") .font(.system(size: 22, weight: .semibold, design: .rounded)) .frame(maxWidth: .infinity, alignment: .leading) .padding([.top, .leading], 16) HStackSnap(alignment: .leading(16)) { - + ForEach(TripTupleModel.exampleModels) { viewModel in TripTupleView(viewModel: viewModel) .frame(maxWidth: 250) .snapAlignmentHelper(id: viewModel.id) } - } onSwipe: { index in - - print(index) + } eventHandler: { event in + + handleSnapToScrollEvent(event: event) } .frame(height: 130) .padding(.top, 4) } } + + func handleSnapToScrollEvent(event: SnapToScrollEvent) { + + switch event { + + case let .didLayout(layoutInfo: layoutInfo): + + print("\(layoutInfo.keys.count) items layed out") + + case let .swipe(index: index): + + print("swiped to index: \(index)") + } + } } // MARK: - Example2ContentView_Previews diff --git a/SnapToScrollDemo/Sources/Example3/Example3ContentView.swift b/SnapToScrollDemo/Sources/Example3/Example3ContentView.swift index 63683a2..fc39767 100644 --- a/SnapToScrollDemo/Sources/Example3/Example3ContentView.swift +++ b/SnapToScrollDemo/Sources/Example3/Example3ContentView.swift @@ -5,12 +5,10 @@ import SwiftUI struct Example3ContentView: View { - // MARK: Internal - var body: some View { - + VStack { - + Text("Getting Started") .font(.system(size: 22, weight: .semibold, design: .rounded)) .foregroundColor(.white) @@ -18,7 +16,7 @@ struct Example3ContentView: View { .padding([.top, .leading], 32) HStackSnap(alignment: .center(32)) { - + ForEach(GettingStartedModel.exampleModels) { viewModel in GettingStartedView( @@ -26,9 +24,9 @@ struct Example3ContentView: View { viewModel: viewModel) .snapAlignmentHelper(id: viewModel.id) } - } onSwipe: { index in + } eventHandler: { event in - selectedGettingStartedIndex = index + handleSnapToScrollEvent(event: event) } .frame(height: 200) .padding(.top, 4) @@ -40,6 +38,19 @@ struct Example3ContentView: View { endPoint: .bottom)) } + func handleSnapToScrollEvent(event: SnapToScrollEvent) { + switch event { + case let .didLayout(layoutInfo: layoutInfo): + + print("\(layoutInfo.keys.count) items layed out") + + case let .swipe(index: index): + + print("swiped to index: \(index)") + selectedGettingStartedIndex = index + } + } + // MARK: Private @State private var selectedGettingStartedIndex: Int = 0 diff --git a/Sources/HStackSnap.swift b/Sources/HStackSnap.swift index 3ff75ba..14b87a4 100644 --- a/Sources/HStackSnap.swift +++ b/Sources/HStackSnap.swift @@ -3,21 +3,19 @@ import SwiftUI public struct HStackSnap: View { - public typealias SwipeEventHandler = ((Int) -> Void) - // MARK: Lifecycle public init( alignment: SnapAlignment, coordinateSpace: String = "SnapToScroll", @ViewBuilder content: @escaping () -> Content, - onSwipe: SwipeEventHandler? = .none) { + eventHandler: SnapToScrollEventHandler? = .none) { self.content = content - self.alignment = alignment - leadingOffset = alignment.scrollOffset + self.alignment = alignment + self.leadingOffset = alignment.scrollOffset self.coordinateSpace = coordinateSpace - swipeEventHandler = onSwipe + self.eventHandler = eventHandler } // MARK: Public @@ -36,7 +34,7 @@ public struct HStackSnap: View { leadingOffset: leadingOffset, coordinateSpace: coordinateSpace, content: content, - onSwipe: swipeEventHandler) + eventHandler: eventHandler) .environmentObject(SizeOverride(itemWidth: alignment.shouldSetWidth ? calculatedItemWidth(parentWidth: geometry.size.width, offset: alignment.scrollOffset) : .none)) } } @@ -52,7 +50,7 @@ public struct HStackSnap: View { /// Calculated offset based on `SnapLocation` private let leadingOffset: CGFloat - private var swipeEventHandler: SwipeEventHandler? + private var eventHandler: SnapToScrollEventHandler? private let coordinateSpace: String } diff --git a/Sources/Model/SnapToScrollEvent.swift b/Sources/Model/SnapToScrollEvent.swift new file mode 100644 index 0000000..9c44bbc --- /dev/null +++ b/Sources/Model/SnapToScrollEvent.swift @@ -0,0 +1,11 @@ +import Foundation +import SwiftUI + +public enum SnapToScrollEvent { + + /// Swiped to index. + case swipe(index: Int) + + /// HStackSnap completed layout calculations. (item index, item leading offset) + case didLayout(layoutInfo: [Int: CGFloat]) +} diff --git a/Sources/Views/HStackSnapCore.swift b/Sources/Views/HStackSnapCore.swift index 1257594..a390fba 100644 --- a/Sources/Views/HStackSnapCore.swift +++ b/Sources/Views/HStackSnapCore.swift @@ -1,33 +1,32 @@ import Foundation import SwiftUI -public struct HStackSnapCore: View { +public typealias SnapToScrollEventHandler = ((SnapToScrollEvent) -> Void) - public typealias SwipeEventHandler = ((Int) -> Void) +// MARK: - HStackSnapCore +public struct HStackSnapCore: View { // MARK: Lifecycle public init( leadingOffset: CGFloat, coordinateSpace: String = "SnapToScroll", @ViewBuilder content: @escaping () -> Content, - onSwipe: SwipeEventHandler? = .none) { - + eventHandler: SnapToScrollEventHandler? = .none) { + self.content = content - targetOffset = leadingOffset - scrollOffset = leadingOffset + self.targetOffset = leadingOffset + self.scrollOffset = leadingOffset self.coordinateSpace = coordinateSpace - swipeEventHandler = onSwipe + self.eventHandler = eventHandler } // MARK: Public public var body: some View { - GeometryReader { geometry in HStack { - HStack(content: content) .offset(x: scrollOffset, y: .zero) .animation(.easeOut(duration: 0.2)) @@ -41,17 +40,15 @@ public struct HStackSnapCore: View { // Calculate all values once, on render. On-the-fly calculations with GeometryReader // proved occasionally unstable in testing. if !hasCalculatedFrames { - let screenWidth = geometry.frame(in: .named(coordinateSpace)).width var itemScrollPositions: [Int: CGFloat] = [:] var frameMaxXVals: [CGFloat] = [] - for pref in preferences { - - itemScrollPositions[pref.id.hashValue] = scrollOffset(for: pref.rect.minX) - frameMaxXVals.append(pref.rect.maxX) + for (index, preference) in preferences.enumerated() { + itemScrollPositions[index] = scrollOffset(for: preference.rect.minX) + frameMaxXVals.append(preference.rect.maxX) } // Array of content widths from currentElement.minX to lastElement.maxX @@ -59,7 +56,6 @@ public struct HStackSnapCore: View { // Calculate content widths (used to trim snap positions later) for currMinX in preferences.map({ $0.rect.minX }) { - guard let maxX = preferences.last?.rect.maxX else { break } let widthToEnd = maxX - currMinX @@ -71,9 +67,7 @@ public struct HStackSnapCore: View { // Calculate how many snap locations should be trimmed. for i in 0 ..< reversedFitMap.count { - if reversedFitMap[i] > screenWidth { - frameTrim = max(i - 1, 0) break } @@ -81,14 +75,16 @@ public struct HStackSnapCore: View { // Write valid snap locations to state. for (i, item) in itemScrollPositions.sorted(by: { $0.value > $1.value }) - .enumerated() { - + .enumerated() + { guard i < (itemScrollPositions.count - frameTrim) else { break } snapLocations[item.key] = item.value } hasCalculatedFrames = true + + eventHandler?(.didLayout(layoutInfo: itemScrollPositions)) } }) .gesture(snapDrag) @@ -101,21 +97,18 @@ public struct HStackSnapCore: View { var content: () -> Content var snapDrag: some Gesture { - DragGesture() .onChanged { gesture in self.scrollOffset = gesture.translation.width + prevScrollOffset - }.onEnded { event in + }.onEnded { _ in let currOffset = scrollOffset var closestSnapLocation: CGFloat = snapLocations.first?.value ?? targetOffset // Calculate closest snap location for (_, offset) in snapLocations { - if abs(offset - currOffset) < abs(closestSnapLocation - currOffset) { - closestSnapLocation = offset } } @@ -125,8 +118,7 @@ public struct HStackSnapCore: View { .firstIndex(of: closestSnapLocation) ?? 0 if selectedIndex != previouslySentIndex { - - swipeEventHandler?(selectedIndex) + eventHandler?(.swipe(index: selectedIndex)) previouslySentIndex = selectedIndex } @@ -135,9 +127,8 @@ public struct HStackSnapCore: View { prevScrollOffset = scrollOffset } } - - func scrollOffset(for x: CGFloat) -> CGFloat { + func scrollOffset(for x: CGFloat) -> CGFloat { return (targetOffset * 2) - x } @@ -157,7 +148,7 @@ public struct HStackSnapCore: View { /// The original offset of each frame, used to calculate `scrollOffset` @State private var snapLocations: [Int: CGFloat] = [:] - private var swipeEventHandler: SwipeEventHandler? + private var eventHandler: SnapToScrollEventHandler? @State private var previouslySentIndex: Int = 0