Skip to content

Feat/snap to center #1

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 5 commits into from
Sep 1, 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
43 changes: 26 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,56 @@ https://user-images.githubusercontent.com/8763719/131393666-6af82d2a-998e-4dba-a

## Getting Started

Using `SnapToScroll` is straightforward. There's only two requirements:
Using `SnapToScroll` is straightforward. There's just three steps.

1. Replace `HStack` with `HStackSnap`
2. Add `GeometryReaderOverlay` to your view.
1. Import `SnapToScroll`
2. Replace `HStack` with `HStackSnap`
3. Add `.snapAlignmentHelper` to your view.

An example:

```swift
import SnapToScroll // Step 1
...

HStackSnap(leadingOffset: 16) {
HStackSnap(alignment: .center(32)) { // Step 2

ForEach(modelArray) { viewModel in
ForEach(myModels) { viewModel in

myView(viewModel: viewModel)
.frame(maxWidth: 250)
.overlay(GeometryReaderOverlay(id: viewModel.id))
}
}
MyView(
selectedIndex: $selectedIndex,
viewModel: viewModel
)
.snapAlignmentHelper(id: viewModel.id) // Step 3
}
}

```
For more examples, see `SnapToScrollDemo/ContentView.swift`.

## Options
## Configuration

`HStackSnap` comes with two customizable properties:

- `leadingOffset`: The leading padding you'd like each element to snap to.
- `coordinateSpace`: Option to set custom name for the coordinate space, in the case you're using multiple `HStackSnap`s of various sizes.
- `alignment`: The way you'd like your elements to be arranged.
- `leading(CGFloat)`: Aligns your child views to the leading edge of `HStackSnap`. This configuration supports elements of various sizes, so long as they don't take up all available horizontal space (which would extend beyond the screen). Use the value to set the size of the left offset.
- `center(CGFloat)`: Automatically aligns your child view to the center of the screen, using the offset value you've provided. This is accomplished with inside of the `.snapAlignmentHelper` which sets the frame width based on the available space. Note that setting your own width elsewhere may produce unexpected layouts.
- `coordinateSpace`: Option to set custom name for the coordinate space, in the case you're using multiple `HStackSnap`s of various sizes. If you use this, set the same value in `.snapAlignmentHelper`.

`.snapAlignmentHelper` comes with two options as well:

- `id`: Required. A unique ID for the element.
- `coordinateSpace`: Same as above.

## Limitations

1. If your child views are designed to take up all available horizontal space, they'll expand beyond the visible view. Prevent this with `.frame(maxWidth: x)`
2. `HStackSnap` is currently designed to work with static content.
3. At the moment, `HStackSnap` offers snapping to the leading edge only. If you'd like to offer a PR that adds support for `.center` and / or `.trailing`, I'd love to look it over!
- `HStackSnap` is currently designed to work with static content.

## How it Works

At render, `HStackSnap` reads the frame data of each child element and calculates the `scrollOffset` each element should use. Then, on `DragGesture.onEnded`, the nearest snap location is calculated, and the scroll offset is set to this point.

Read through `HStackSnap` for more details.
Read through `HStackSnap.swift` and `Views/HStackSnapCore.swift` for more details.

## Credits

Expand Down
28 changes: 28 additions & 0 deletions SnapToScrollDemo/SnapToScrollDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
objects = {

/* Begin PBXBuildFile section */
840130AF26DFB8DD00E4A8A3 /* GettingStartedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840130AE26DFB8DC00E4A8A3 /* GettingStartedModel.swift */; };
840130B126DFB93400E4A8A3 /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840130B026DFB93400E4A8A3 /* GettingStartedView.swift */; };
840130B326DFC27700E4A8A3 /* Example3ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840130B226DFC27700E4A8A3 /* Example3ContentView.swift */; };
840130B526DFC2FB00E4A8A3 /* Example2ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840130B426DFC2FB00E4A8A3 /* Example2ContentView.swift */; };
840130B726DFC33100E4A8A3 /* Example1ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840130B626DFC33100E4A8A3 /* Example1ContentView.swift */; };
841B0F8326DD39A4008A436B /* TripTupleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841B0F8226DD39A4008A436B /* TripTupleView.swift */; };
84B568D926DD271000D37CF2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B568D826DD271000D37CF2 /* TagView.swift */; };
84B568DB26DD309400D37CF2 /* Example1HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B568DA26DD309400D37CF2 /* Example1HeaderView.swift */; };
Expand All @@ -23,6 +28,11 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
840130AE26DFB8DC00E4A8A3 /* GettingStartedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedModel.swift; sourceTree = "<group>"; };
840130B026DFB93400E4A8A3 /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
840130B226DFC27700E4A8A3 /* Example3ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example3ContentView.swift; sourceTree = "<group>"; };
840130B426DFC2FB00E4A8A3 /* Example2ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example2ContentView.swift; sourceTree = "<group>"; };
840130B626DFC33100E4A8A3 /* Example1ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example1ContentView.swift; sourceTree = "<group>"; };
841B0F8226DD39A4008A436B /* TripTupleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripTupleView.swift; sourceTree = "<group>"; };
84B568D826DD271000D37CF2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
84B568DA26DD309400D37CF2 /* Example1HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example1HeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -52,9 +62,20 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
840130AD26DFB8C800E4A8A3 /* Example3 */ = {
isa = PBXGroup;
children = (
840130B226DFC27700E4A8A3 /* Example3ContentView.swift */,
840130AE26DFB8DC00E4A8A3 /* GettingStartedModel.swift */,
840130B026DFB93400E4A8A3 /* GettingStartedView.swift */,
);
path = Example3;
sourceTree = "<group>";
};
84B568D626DD26A800D37CF2 /* Example1 */ = {
isa = PBXGroup;
children = (
840130B626DFC33100E4A8A3 /* Example1ContentView.swift */,
84C99CD726D99BA100C1D5C4 /* TagModel.swift */,
84B568D826DD271000D37CF2 /* TagView.swift */,
84B568DA26DD309400D37CF2 /* Example1HeaderView.swift */,
Expand All @@ -68,6 +89,7 @@
84B568DD26DD351900D37CF2 /* TripModel.swift */,
84B568DF26DD37A600D37CF2 /* TripView.swift */,
841B0F8226DD39A4008A436B /* TripTupleView.swift */,
840130B426DFC2FB00E4A8A3 /* Example2ContentView.swift */,
);
path = "Example 2";
sourceTree = "<group>";
Expand Down Expand Up @@ -97,6 +119,7 @@
84D9FA3526D9753600F87EF5 /* AppDelegate.swift */,
84B568D626DD26A800D37CF2 /* Example1 */,
84B568DC26DD311F00D37CF2 /* Example 2 */,
840130AD26DFB8C800E4A8A3 /* Example3 */,
84D9FA3726D9753600F87EF5 /* SceneDelegate.swift */,
84D9FA3926D9753600F87EF5 /* ContentView.swift */,
);
Expand Down Expand Up @@ -204,10 +227,15 @@
files = (
84B568DB26DD309400D37CF2 /* Example1HeaderView.swift in Sources */,
84D9FA3626D9753600F87EF5 /* AppDelegate.swift in Sources */,
840130AF26DFB8DD00E4A8A3 /* GettingStartedModel.swift in Sources */,
841B0F8326DD39A4008A436B /* TripTupleView.swift in Sources */,
840130B726DFC33100E4A8A3 /* Example1ContentView.swift in Sources */,
84B568D926DD271000D37CF2 /* TagView.swift in Sources */,
840130B326DFC27700E4A8A3 /* Example3ContentView.swift in Sources */,
84B568E026DD37A600D37CF2 /* TripView.swift in Sources */,
840130B526DFC2FB00E4A8A3 /* Example2ContentView.swift in Sources */,
84B568DE26DD351900D37CF2 /* TripModel.swift in Sources */,
840130B126DFB93400E4A8A3 /* GettingStartedView.swift in Sources */,
84D9FA3826D9753600F87EF5 /* SceneDelegate.swift in Sources */,
84D9FA3A26D9753600F87EF5 /* ContentView.swift in Sources */,
84C99CD826D99BA100C1D5C4 /* TagModel.swift in Sources */,
Expand Down
49 changes: 11 additions & 38 deletions SnapToScrollDemo/Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,28 @@ import SwiftUI

struct ContentView: View {

@State var items = [("one", 1), ("two", 2), ("three", 3), ("four", 4), ("five", 5), ("six", 6)]

var body: some View {
VStack {

ScrollView {

VerticalSpace

VStack {

LargeHeader(text: "Example 1")

Example1HeaderView()

// Don't forget to attach GeometryReaderOverlay!
HStackSnap(leadingOffset: 16) {

ForEach(TagModel.exampleModels) { viewModel in

TagView(viewModel: viewModel)
.overlay(GeometryReaderOverlay(id: viewModel.id))
}
}.padding(.top, 4)
}
LargeHeader(text: "Example 1")

Example1ContentView()

VerticalSpace

VStack {

LargeHeader(text: "Example 2")

Text("Explore Nearby")
.font(.system(size: 22, weight: .semibold, design: .rounded))
.frame(maxWidth: .infinity, alignment: .leading)
.padding([.top, .leading], 16)

HStackSnap(leadingOffset: 16) {

ForEach(TripTupleModel.exampleModels) { viewModel in
LargeHeader(text: "Example 2")

Example2ContentView()

VerticalSpace

TripTupleView(viewModel: viewModel)
.frame(maxWidth: 250)
.overlay(GeometryReaderOverlay(id: viewModel.id))
}
}
.frame(height: 200)
.padding(.top, 4)
}
LargeHeader(text: "Example 3")

Example3ContentView()
}
}
.preferredColorScheme(.light)
Expand Down
40 changes: 40 additions & 0 deletions SnapToScrollDemo/Sources/Example 2/Example2ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import SwiftUI
import SnapToScroll

// 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)
}
.frame(height: 130)
.padding(.top, 4)
}
}
}

// MARK: - Example2ContentView_Previews

struct Example2ContentView_Previews: PreviewProvider {
static var previews: some View {
Example2ContentView()
}
}
35 changes: 35 additions & 0 deletions SnapToScrollDemo/Sources/Example1/Example1ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Example2ContentView.swift
// Example2ContentView
//
// Created by Trent Guillory on 9/1/21.
//

import SwiftUI
import SnapToScroll

struct Example1ContentView: View {
var body: some View {

VStack {

Example1HeaderView()

// Don't forget to attach snapAlignmentHelper!
HStackSnap(alignment: .leading(16)) {

ForEach(TagModel.exampleModels) { viewModel in

TagView(viewModel: viewModel)
.snapAlignmentHelper(id: viewModel.id)
}
}.padding(.top, 4)
}
}
}

struct Example1ContentView_Previews: PreviewProvider {
static var previews: some View {
Example2ContentView()
}
}
54 changes: 54 additions & 0 deletions SnapToScrollDemo/Sources/Example3/Example3ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import SnapToScroll
import SwiftUI

// MARK: - Example3ContentView

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

selectedGettingStartedIndex = index
}
.frame(height: 200)
.padding(.top, 4)
}
.padding([.top, .bottom], 64)
.background(LinearGradient(
colors: [Color("Cream"), Color("LightPink")],
startPoint: .top,
endPoint: .bottom))
}

// MARK: Private

@State private var selectedGettingStartedIndex: Int = 0
}

// MARK: - Example3ContentView_Previews

struct Example3ContentView_Previews: PreviewProvider {
static var previews: some View {
Example3ContentView()
}
}
32 changes: 32 additions & 0 deletions SnapToScrollDemo/Sources/Example3/GettingStartedModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

struct GettingStartedModel: Identifiable {

static let exampleModels: [GettingStartedModel] = [
.init(
id: 0,
systemImage: "camera.aperture",
title: "Snap a Pic",
body: "We feature the viewfinder front and center in this throwback app."),
.init(
id: 1,
systemImage: "camera.filters",
title: "Filter it Up",
body: "Add filters - from detailed colorization to film effects."),
.init(
id: 2,
systemImage: "paperplane",
title: "Send It",
body: "Share your photos with your contacts. Or the entire world."),
.init(
id: 3,
systemImage: "sparkles",
title: "Be Awesome",
body: "You're clearly already doing this. Just wanted to remind you. 😉"),
]

let id: Int
let systemImage: String
let title: String
let body: String
}
Loading