Skip to content

Commit

Permalink
Merge pull request #4 from swiftui-library/feat/metadata-callback
Browse files Browse the repository at this point in the history
add callback for view layout
  • Loading branch information
trentguillory authored Nov 24, 2021
2 parents 2153c7d + 845bf70 commit 2634703
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 50 deletions.
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

0 comments on commit 2634703

Please sign in to comment.