Skip to content

Commit 01a0879

Browse files
authored
Merge pull request #5 from fermoya/feat/adding-alignment
Feat/adding alignment
2 parents 0a1109a + 7103e78 commit 01a0879

File tree

9 files changed

+197
-83
lines changed

9 files changed

+197
-83
lines changed

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
SwiftUIPager provides a `Pager` component built with SwiftUI native components. `Pager` is a view that renders a scrollable container to display a handful of pages. These pages are recycled on scroll, so you don't have to worry about memory issues.
88

9+
Create vertical or horizontal pagers, align the cards, change the direction of the scroll, animate the pagintation... `Pager` lets you do anything you want.
10+
911
<img src="resources/example-of-usage.gif" alt="Example of usage"/>
1012

1113
## Requirements
@@ -33,12 +35,14 @@ Go to XCode:
3335

3436
Creating a `Pager` is very simple. You just need to pass:
3537
- `Binding` to page index
36-
- Array of items that conform to `Equatable` and `Identifiable`
38+
- `Array` of items
39+
- `KeyPath` to an identifier.
3740
- `ViewBuilder` factory method to create each page
3841

3942
```swift
4043
Pager(page: self.$pageIndex,
4144
data: self.items,
45+
id: \.identifier,
4246
content: { item in
4347
// create a page based on the data passed
4448
self.pageView(item)
@@ -47,7 +51,7 @@ Creating a `Pager` is very simple. You just need to pass:
4751

4852
### UI customization
4953

50-
`Pager` is easily customizable through a number of view-modifier functions. You can change the vertical insets, spacing between items or the page aspect ratio, among others:
54+
`Pager` is easily customizable through a number of view-modifier functions. You can change the orientation, the direction of the scroll, the alignment, the space between items or the page aspect ratio, among others:
5155

5256
```swift
5357
Pager(...)
@@ -72,9 +76,21 @@ Pager(...)
7276

7377
<img src="resources/vertical-pager.gif" alt="PageAspectRatio greater than 1" height="640"/>
7478

79+
You can customize the alignment and the direction of the scroll. For instance, you can have a horizontal `Pager` that scrolls right-to-left that it's aligned at the start of the scroll:
80+
81+
```swift
82+
Pager(...)
83+
.itemSpacing(10)
84+
.alignment(.start)
85+
.horizontal(.rightToLeft)
86+
.itemAspectRatio(0.6)
87+
```
88+
89+
<img src="resources/orientation-alignment.gif" alt="PageAspectRatio greater than 1" height="640"/>
90+
7591
### Animations
7692

77-
Use `interactive` to pass a shrink ratio that will be applied to those components that are not focused, that is, those elements whose index is different from `pageIndex` binding:
93+
Use `interactive` add a scale animation effect to those pages that are unfocused, that is, those elements whose index is different from `pageIndex`:
7894

7995
```swift
8096
Pager(...)
@@ -96,9 +112,11 @@ Pager(...)
96112
### Gestures
97113

98114
`Pager` comes with the following built-in gestures:
99-
- Tap on any item to bring it to focus.
115+
- Tap on any item to bring it to focus. Enable this gesture with `itemTappable`
100116
- Swipe acroos the items
101117

118+
You can disable any interaction by calling `disableInteraction`.
119+
102120
### Events
103121

104122
Use `onPageChanged` to react to any change on the page index:

Sample.xcodeproj/project.pbxproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
17D9E0FA23D4CF6900C5AE93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17D9E0F923D4CF6900C5AE93 /* Assets.xcassets */; };
1414
17D9E0FD23D4CF6900C5AE93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17D9E0FC23D4CF6900C5AE93 /* Assets.xcassets */; };
1515
17D9E10023D4CF6900C5AE93 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17D9E0FE23D4CF6900C5AE93 /* LaunchScreen.storyboard */; };
16-
6BB892E023D9F13C00AC9331 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 6BB892DF23D9F13C00AC9331 /* SwiftUIPager */; };
16+
6BEA272523DA3596001A2082 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 6BEA272423DA3596001A2082 /* SwiftUIPager */; };
1717
/* End PBXBuildFile section */
1818

1919
/* Begin PBXFileReference section */
@@ -32,7 +32,7 @@
3232
isa = PBXFrameworksBuildPhase;
3333
buildActionMask = 2147483647;
3434
files = (
35-
6BB892E023D9F13C00AC9331 /* SwiftUIPager in Frameworks */,
35+
6BEA272523DA3596001A2082 /* SwiftUIPager in Frameworks */,
3636
);
3737
runOnlyForDeploymentPostprocessing = 0;
3838
};
@@ -94,7 +94,7 @@
9494
);
9595
name = Sample;
9696
packageProductDependencies = (
97-
6BB892DF23D9F13C00AC9331 /* SwiftUIPager */,
97+
6BEA272423DA3596001A2082 /* SwiftUIPager */,
9898
);
9999
productName = SwiftUIPager;
100100
productReference = 17D9E0F023D4CF6700C5AE93 /* Sample.app */;
@@ -125,7 +125,7 @@
125125
);
126126
mainGroup = 17D9E0E723D4CF6700C5AE93;
127127
packageReferences = (
128-
6BB892DE23D9F13C00AC9331 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
128+
6BEA272323DA3596001A2082 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
129129
);
130130
productRefGroup = 17D9E0F123D4CF6700C5AE93 /* Products */;
131131
projectDirPath = "";
@@ -352,20 +352,20 @@
352352
/* End XCConfigurationList section */
353353

354354
/* Begin XCRemoteSwiftPackageReference section */
355-
6BB892DE23D9F13C00AC9331 /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = {
355+
6BEA272323DA3596001A2082 /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = {
356356
isa = XCRemoteSwiftPackageReference;
357357
repositoryURL = "https://github.com/fermoya/SwiftUIPager";
358358
requirement = {
359-
branch = "fix/page-offset";
359+
branch = "feat/adding-alignment";
360360
kind = branch;
361361
};
362362
};
363363
/* End XCRemoteSwiftPackageReference section */
364364

365365
/* Begin XCSwiftPackageProductDependency section */
366-
6BB892DF23D9F13C00AC9331 /* SwiftUIPager */ = {
366+
6BEA272423DA3596001A2082 /* SwiftUIPager */ = {
367367
isa = XCSwiftPackageProductDependency;
368-
package = 6BB892DE23D9F13C00AC9331 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
368+
package = 6BEA272323DA3596001A2082 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
369369
productName = SwiftUIPager;
370370
};
371371
/* End XCSwiftPackageProductDependency section */

Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sample/ContentView.swift

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,40 @@
99
import SwiftUI
1010
import SwiftUIPager
1111

12-
extension Int: Identifiable {
13-
public var id: Int { return self }
14-
}
15-
1612
struct ContentView: View {
1713

1814
@State var isPresented: Bool = false
1915
@State var pageIndex: Int = 0
20-
var data: [Int] = Array((0...20))
16+
var data: [Int] = Array((0...5))
2117

2218
var body: some View {
23-
Button(action: {
24-
self.isPresented.toggle()
25-
}, label: {
26-
Text("Tap me")
27-
}).sheet(isPresented: $isPresented, content: {
28-
self.presentedView
29-
})
30-
}
31-
32-
var presentedView: some View {
3319
GeometryReader { proxy in
34-
ScrollView {
35-
VStack {
36-
Pager(page: self.$pageIndex,
37-
data: self.data,
38-
content: { index in
39-
self.pageView(index)
40-
.cornerRadius(10)
41-
.shadow(radius: 5)
42-
})
43-
.interactive(0.8)
44-
.itemSpacing(10)
45-
.padding(8)
46-
.itemAspectRatio(0.8)
47-
.itemTappable(true)
48-
.frame(width: min(proxy.size.width,
49-
proxy.size.height),
50-
height: min(proxy.size.width,
51-
proxy.size.height))
52-
.border(Color.red, width: 2)
53-
ForEach(self.data) { i in
54-
Text("Page: \(i)")
55-
.bold()
56-
.padding()
57-
}
58-
}
20+
VStack {
21+
Pager(page: self.$pageIndex,
22+
data: self.data,
23+
id: \.self,
24+
content: { index in
25+
self.pageView(index)
26+
.cornerRadius(10)
27+
.shadow(radius: 5)
28+
})
29+
.itemSpacing(10)
30+
.alignment(.start)
31+
.horizontal(.rightToLeft)
32+
.itemAspectRatio(0.6)
33+
.frame(width: min(proxy.size.width,
34+
proxy.size.height),
35+
height: min(proxy.size.width,
36+
proxy.size.height))
37+
.border(Color.red, width: 2)
38+
Spacer()
39+
Text("Page: \(self.pageIndex)")
40+
.bold()
41+
Spacer()
5942
}
6043
}
6144
}
45+
6246
}
6347

6448
extension ContentView {

Sources/SwiftUIPager/Pager+Buildable.swift

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,53 @@ import SwiftUI
1010

1111
extension Pager: Buildable {
1212

13+
/// Swipe direction for a vertical `Pager`
14+
public enum HorizontalSwipeDirection {
15+
16+
/// Pages move from left to right
17+
case leftToRight
18+
19+
/// Pages move from right to left
20+
case rightToLeft
21+
}
22+
23+
/// Swipe direction for a horizontal `Pager`
24+
public enum VerticalSwipeDirection {
25+
26+
/// Pages move from top left to bottom
27+
case topToBottom
28+
29+
/// Pages move from bottom to top
30+
case bottomToTop
31+
}
32+
33+
/// Changes the a the alignment of the pages relative to their container
34+
public func alignment(_ value: Alignment) -> Self {
35+
mutating(keyPath: \.alignment, value: value)
36+
}
37+
1338
/// Adds a `TapGesture` to the items to bring them to focus
1439
public func itemTappable(_ value: Bool) -> Self {
1540
mutating(keyPath: \.isItemTappable, value: value)
1641
}
1742

43+
/// Disables any gesture interaction
44+
public func disableInteraction(_ value: Bool) -> Self {
45+
mutating(keyPath: \.isUserInteractionEnabled, value: value)
46+
}
47+
1848
/// Returns a horizontal pager
19-
public func horizontal() -> Self {
20-
mutating(keyPath: \.isHorizontal, value: true)
49+
public func horizontal(_ swipeDirection: HorizontalSwipeDirection = .leftToRight) -> Self {
50+
let scrollDirectionAngle: Angle = swipeDirection == .leftToRight ? .zero : Angle(degrees: 180)
51+
return mutating(keyPath: \.isHorizontal, value: true)
52+
.mutating(keyPath: \.scrollDirectionAngle, value: scrollDirectionAngle)
2153
}
2254

2355
/// Returns a vertical pager
24-
public func vertical() -> Self {
25-
mutating(keyPath: \.isHorizontal, value: false)
56+
public func vertical(_ swipeDirection: VerticalSwipeDirection = .topToBottom) -> Self {
57+
let scrollDirectionAngle: Angle = swipeDirection == .topToBottom ? .zero : Angle(degrees: 180)
58+
return mutating(keyPath: \.isHorizontal, value: false)
59+
.mutating(keyPath: \.scrollDirectionAngle, value: scrollDirectionAngle)
2660
}
2761

2862
/// Call this method to provide a shrink ratio that will apply to the items that are not focused.

Sources/SwiftUIPager/Pager+Helper.swift

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ extension Pager {
4747
/// Minimum offset allowed. This allows a bounce offset
4848
var offsetLowerbound: CGFloat {
4949
guard currentPage == 0 else { return CGFloat(numberOfPages) * self.size.width }
50-
return CGFloat(numberOfPagesDisplayed) / 2 * pageDistance - pageDistance / 4
50+
return CGFloat(numberOfPagesDisplayed) / 2 * pageDistance - pageDistance / 4 + alignmentOffset
5151
}
5252

5353
/// Maximum offset allowed. This allows a bounce offset
5454
var offsetUpperbound: CGFloat {
5555
guard currentPage == numberOfPages - 1 else { return -CGFloat(numberOfPages) * self.size.width }
56-
return -CGFloat(numberOfPagesDisplayed) / 2 * pageDistance + pageDistance / 4
56+
return -CGFloat(numberOfPagesDisplayed) / 2 * pageDistance + pageDistance / 4 + alignmentOffset
5757
}
5858

5959
/// Addition of `draggingOffset` and `contentOffset`
@@ -107,40 +107,63 @@ extension Pager {
107107
return min(numberOfPages, maximumNumberOfPages + page)
108108
}
109109

110+
/// Extra offset to complentate the alignment
111+
var alignmentOffset: CGFloat {
112+
let offset: CGFloat
113+
switch alignment {
114+
case .center:
115+
offset = 0
116+
case .end(let insets):
117+
if isVertical {
118+
offset = (size.height - pageSize.height) / 2 - insets
119+
} else {
120+
offset = (size.width - pageSize.width) / 2 - insets
121+
}
122+
case .start(let insets):
123+
if isVertical {
124+
offset = -(size.height - pageSize.height) / 2 + insets
125+
} else {
126+
offset = -(size.width - pageSize.width) / 2 + insets
127+
}
128+
}
129+
130+
return offset
131+
}
132+
110133
/// Offset applied to `HStack`. It's limitted by `offsetUpperbound` and `offsetUpperbound`
111134
var xOffset: CGFloat {
112135
let page = CGFloat(self.page - lowerPageDisplayed)
113136
let numberOfPages = CGFloat(numberOfPagesDisplayed)
114137
let xIncrement = pageDistance / 2
115-
let offset = (numberOfPages / 2 - page) * pageDistance - xIncrement + totalOffset
138+
let offset = (numberOfPages / 2 - page) * pageDistance - xIncrement + totalOffset + alignmentOffset
116139
return max(offsetUpperbound, min(offsetLowerbound, offset))
117140
}
118141

119142
/// Angle for the 3D rotation effect
120143
func angle(for item: Element) -> Angle {
121144
guard shouldRotate else { return .zero }
122145
guard let index = data.firstIndex(of: item) else { return .zero }
123-
146+
124147
let totalIncrement = abs(totalOffset / pageDistance)
125-
148+
126149
let currentAngle = index == page ? .zero : index < page ? Angle(degrees: rotationDegrees) : Angle(degrees: -rotationDegrees)
127150
guard isDragging else {
128151
return currentAngle
129152
}
130-
153+
131154
let newAngle = direction == .forward ? Angle(degrees: currentAngle.degrees + rotationDegrees * Double(totalIncrement)) : Angle(degrees: currentAngle.degrees - rotationDegrees * Double(totalIncrement) )
132155
return newAngle
133156
}
134-
157+
135158
/// Axis for the rotations effect
136159
func axis(for item: Element) -> (CGFloat, CGFloat, CGFloat) {
137160
guard shouldRotate else { return (0, 0, 0) }
138161
guard let index = data.firstIndex(of: item) else { return (0, 0, 0) }
139-
162+
140163
let currentXAxis: CGFloat = index == page ? 0 : index < page ? rotationAxis.x : -rotationAxis.x
141164
return (currentXAxis, rotationAxis.y, rotationAxis.z)
142165
}
143-
166+
144167
/// Scale that applies to a particular item
145168
func scale(for item: Element) -> CGFloat {
146169
guard isDragging else { return isFocused(item) ? 1 : interactiveScale }

0 commit comments

Comments
 (0)