Skip to content

add callback for view layout #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions SnapToScrollDemo/Sources/Example 2/Example2ContentView.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 18 additions & 7 deletions SnapToScrollDemo/Sources/Example3/Example3ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,28 @@ 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)
.frame(maxWidth: .infinity, alignment: .leading)
.padding([.top, .leading], 32)

HStackSnap(alignment: .center(32)) {

ForEach(GettingStartedModel.exampleModels) { viewModel in

GettingStartedView(
selectedIndex: $selectedGettingStartedIndex,
viewModel: viewModel)
.snapAlignmentHelper(id: viewModel.id)
}
} onSwipe: { index in
} eventHandler: { event in

selectedGettingStartedIndex = index
handleSnapToScrollEvent(event: event)
}
.frame(height: 200)
.padding(.top, 4)
Expand All @@ -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
Expand Down
14 changes: 6 additions & 8 deletions Sources/HStackSnap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@ import SwiftUI

public struct HStackSnap<Content: View>: 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
Expand All @@ -36,7 +34,7 @@ public struct HStackSnap<Content: View>: 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))
}
}
Expand All @@ -52,7 +50,7 @@ public struct HStackSnap<Content: View>: View {
/// Calculated offset based on `SnapLocation`
private let leadingOffset: CGFloat

private var swipeEventHandler: SwipeEventHandler?
private var eventHandler: SnapToScrollEventHandler?

private let coordinateSpace: String
}
11 changes: 11 additions & 0 deletions Sources/Model/SnapToScrollEvent.swift
Original file line number Diff line number Diff line change
@@ -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])
}
47 changes: 19 additions & 28 deletions Sources/Views/HStackSnapCore.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
import Foundation
import SwiftUI

public struct HStackSnapCore<Content: View>: View {
public typealias SnapToScrollEventHandler = ((SnapToScrollEvent) -> Void)

public typealias SwipeEventHandler = ((Int) -> Void)
// MARK: - HStackSnapCore

public struct HStackSnapCore<Content: View>: 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))
Expand All @@ -41,25 +40,22 @@ public struct HStackSnapCore<Content: View>: 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
var contentFitMap: [CGFloat] = []

// 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

Expand All @@ -71,24 +67,24 @@ public struct HStackSnapCore<Content: View>: 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
}
}

// 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)
Expand All @@ -101,21 +97,18 @@ public struct HStackSnapCore<Content: View>: 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
}
}
Expand All @@ -125,8 +118,7 @@ public struct HStackSnapCore<Content: View>: View {
.firstIndex(of: closestSnapLocation) ?? 0

if selectedIndex != previouslySentIndex {

swipeEventHandler?(selectedIndex)
eventHandler?(.swipe(index: selectedIndex))
previouslySentIndex = selectedIndex
}

Expand All @@ -135,9 +127,8 @@ public struct HStackSnapCore<Content: View>: View {
prevScrollOffset = scrollOffset
}
}

func scrollOffset(for x: CGFloat) -> CGFloat {

func scrollOffset(for x: CGFloat) -> CGFloat {
return (targetOffset * 2) - x
}

Expand All @@ -157,7 +148,7 @@ public struct HStackSnapCore<Content: View>: 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

Expand Down