diff --git a/README.md b/README.md
index 6adb068..b0d4b3b 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,8 @@
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.
+Create vertical or horizontal pagers, align the cards, change the direction of the scroll, animate the pagintation... `Pager` lets you do anything you want.
+
## Requirements
@@ -33,12 +35,14 @@ Go to XCode:
Creating a `Pager` is very simple. You just need to pass:
- `Binding` to page index
-- Array of items that conform to `Equatable` and `Identifiable`
+- `Array` of items
+- `KeyPath` to an identifier.
- `ViewBuilder` factory method to create each page
```swift
Pager(page: self.$pageIndex,
data: self.items,
+ id: \.identifier,
content: { item in
// create a page based on the data passed
self.pageView(item)
@@ -47,7 +51,7 @@ Creating a `Pager` is very simple. You just need to pass:
### UI customization
-`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:
+`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:
```swift
Pager(...)
@@ -72,9 +76,21 @@ Pager(...)
+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:
+
+```swift
+Pager(...)
+ .itemSpacing(10)
+ .alignment(.start)
+ .horizontal(.rightToLeft)
+ .itemAspectRatio(0.6)
+```
+
+
+
### Animations
-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:
+Use `interactive` add a scale animation effect to those pages that are unfocused, that is, those elements whose index is different from `pageIndex`:
```swift
Pager(...)
@@ -96,9 +112,11 @@ Pager(...)
### Gestures
`Pager` comes with the following built-in gestures:
-- Tap on any item to bring it to focus.
+- Tap on any item to bring it to focus. Enable this gesture with `itemTappable`
- Swipe acroos the items
+You can disable any interaction by calling `disableInteraction`.
+
### Events
Use `onPageChanged` to react to any change on the page index:
diff --git a/Sample.xcodeproj/project.pbxproj b/Sample.xcodeproj/project.pbxproj
index 4f21ba8..323fa25 100644
--- a/Sample.xcodeproj/project.pbxproj
+++ b/Sample.xcodeproj/project.pbxproj
@@ -13,7 +13,7 @@
17D9E0FA23D4CF6900C5AE93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17D9E0F923D4CF6900C5AE93 /* Assets.xcassets */; };
17D9E0FD23D4CF6900C5AE93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17D9E0FC23D4CF6900C5AE93 /* Assets.xcassets */; };
17D9E10023D4CF6900C5AE93 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17D9E0FE23D4CF6900C5AE93 /* LaunchScreen.storyboard */; };
- 6BB892E023D9F13C00AC9331 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 6BB892DF23D9F13C00AC9331 /* SwiftUIPager */; };
+ 6BEA272523DA3596001A2082 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 6BEA272423DA3596001A2082 /* SwiftUIPager */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -32,7 +32,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 6BB892E023D9F13C00AC9331 /* SwiftUIPager in Frameworks */,
+ 6BEA272523DA3596001A2082 /* SwiftUIPager in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -94,7 +94,7 @@
);
name = Sample;
packageProductDependencies = (
- 6BB892DF23D9F13C00AC9331 /* SwiftUIPager */,
+ 6BEA272423DA3596001A2082 /* SwiftUIPager */,
);
productName = SwiftUIPager;
productReference = 17D9E0F023D4CF6700C5AE93 /* Sample.app */;
@@ -125,7 +125,7 @@
);
mainGroup = 17D9E0E723D4CF6700C5AE93;
packageReferences = (
- 6BB892DE23D9F13C00AC9331 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
+ 6BEA272323DA3596001A2082 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
);
productRefGroup = 17D9E0F123D4CF6700C5AE93 /* Products */;
projectDirPath = "";
@@ -352,20 +352,20 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
- 6BB892DE23D9F13C00AC9331 /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = {
+ 6BEA272323DA3596001A2082 /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/fermoya/SwiftUIPager";
requirement = {
- branch = "fix/page-offset";
+ branch = "feat/adding-alignment";
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- 6BB892DF23D9F13C00AC9331 /* SwiftUIPager */ = {
+ 6BEA272423DA3596001A2082 /* SwiftUIPager */ = {
isa = XCSwiftPackageProductDependency;
- package = 6BB892DE23D9F13C00AC9331 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
+ package = 6BEA272323DA3596001A2082 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
productName = SwiftUIPager;
};
/* End XCSwiftPackageProductDependency section */
diff --git a/Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index bea2a7f..16a5092 100644
--- a/Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -5,8 +5,8 @@
"package": "SwiftUIPager",
"repositoryURL": "https://github.com/fermoya/SwiftUIPager",
"state": {
- "branch": "fix/page-offset",
- "revision": "3b05829020ec5e10a0997455b2a7fb18dfb4af53",
+ "branch": "feat/adding-alignment",
+ "revision": "0d5ce99d889b105b600baeb5219d3b90f90667f8",
"version": null
}
}
diff --git a/Sample/ContentView.swift b/Sample/ContentView.swift
index a631980..9fccb6e 100644
--- a/Sample/ContentView.swift
+++ b/Sample/ContentView.swift
@@ -9,56 +9,40 @@
import SwiftUI
import SwiftUIPager
-extension Int: Identifiable {
- public var id: Int { return self }
-}
-
struct ContentView: View {
@State var isPresented: Bool = false
@State var pageIndex: Int = 0
- var data: [Int] = Array((0...20))
+ var data: [Int] = Array((0...5))
var body: some View {
- Button(action: {
- self.isPresented.toggle()
- }, label: {
- Text("Tap me")
- }).sheet(isPresented: $isPresented, content: {
- self.presentedView
- })
- }
-
- var presentedView: some View {
GeometryReader { proxy in
- ScrollView {
- VStack {
- Pager(page: self.$pageIndex,
- data: self.data,
- content: { index in
- self.pageView(index)
- .cornerRadius(10)
- .shadow(radius: 5)
- })
- .interactive(0.8)
- .itemSpacing(10)
- .padding(8)
- .itemAspectRatio(0.8)
- .itemTappable(true)
- .frame(width: min(proxy.size.width,
- proxy.size.height),
- height: min(proxy.size.width,
- proxy.size.height))
- .border(Color.red, width: 2)
- ForEach(self.data) { i in
- Text("Page: \(i)")
- .bold()
- .padding()
- }
- }
+ VStack {
+ Pager(page: self.$pageIndex,
+ data: self.data,
+ id: \.self,
+ content: { index in
+ self.pageView(index)
+ .cornerRadius(10)
+ .shadow(radius: 5)
+ })
+ .itemSpacing(10)
+ .alignment(.start)
+ .horizontal(.rightToLeft)
+ .itemAspectRatio(0.6)
+ .frame(width: min(proxy.size.width,
+ proxy.size.height),
+ height: min(proxy.size.width,
+ proxy.size.height))
+ .border(Color.red, width: 2)
+ Spacer()
+ Text("Page: \(self.pageIndex)")
+ .bold()
+ Spacer()
}
}
}
+
}
extension ContentView {
diff --git a/Sources/SwiftUIPager/Pager+Buildable.swift b/Sources/SwiftUIPager/Pager+Buildable.swift
index de7397b..45fa386 100644
--- a/Sources/SwiftUIPager/Pager+Buildable.swift
+++ b/Sources/SwiftUIPager/Pager+Buildable.swift
@@ -10,19 +10,53 @@ import SwiftUI
extension Pager: Buildable {
+ /// Swipe direction for a vertical `Pager`
+ public enum HorizontalSwipeDirection {
+
+ /// Pages move from left to right
+ case leftToRight
+
+ /// Pages move from right to left
+ case rightToLeft
+ }
+
+ /// Swipe direction for a horizontal `Pager`
+ public enum VerticalSwipeDirection {
+
+ /// Pages move from top left to bottom
+ case topToBottom
+
+ /// Pages move from bottom to top
+ case bottomToTop
+ }
+
+ /// Changes the a the alignment of the pages relative to their container
+ public func alignment(_ value: Alignment) -> Self {
+ mutating(keyPath: \.alignment, value: value)
+ }
+
/// Adds a `TapGesture` to the items to bring them to focus
public func itemTappable(_ value: Bool) -> Self {
mutating(keyPath: \.isItemTappable, value: value)
}
+ /// Disables any gesture interaction
+ public func disableInteraction(_ value: Bool) -> Self {
+ mutating(keyPath: \.isUserInteractionEnabled, value: value)
+ }
+
/// Returns a horizontal pager
- public func horizontal() -> Self {
- mutating(keyPath: \.isHorizontal, value: true)
+ public func horizontal(_ swipeDirection: HorizontalSwipeDirection = .leftToRight) -> Self {
+ let scrollDirectionAngle: Angle = swipeDirection == .leftToRight ? .zero : Angle(degrees: 180)
+ return mutating(keyPath: \.isHorizontal, value: true)
+ .mutating(keyPath: \.scrollDirectionAngle, value: scrollDirectionAngle)
}
/// Returns a vertical pager
- public func vertical() -> Self {
- mutating(keyPath: \.isHorizontal, value: false)
+ public func vertical(_ swipeDirection: VerticalSwipeDirection = .topToBottom) -> Self {
+ let scrollDirectionAngle: Angle = swipeDirection == .topToBottom ? .zero : Angle(degrees: 180)
+ return mutating(keyPath: \.isHorizontal, value: false)
+ .mutating(keyPath: \.scrollDirectionAngle, value: scrollDirectionAngle)
}
/// Call this method to provide a shrink ratio that will apply to the items that are not focused.
diff --git a/Sources/SwiftUIPager/Pager+Helper.swift b/Sources/SwiftUIPager/Pager+Helper.swift
index 6a2bd5b..136be7d 100644
--- a/Sources/SwiftUIPager/Pager+Helper.swift
+++ b/Sources/SwiftUIPager/Pager+Helper.swift
@@ -47,13 +47,13 @@ extension Pager {
/// Minimum offset allowed. This allows a bounce offset
var offsetLowerbound: CGFloat {
guard currentPage == 0 else { return CGFloat(numberOfPages) * self.size.width }
- return CGFloat(numberOfPagesDisplayed) / 2 * pageDistance - pageDistance / 4
+ return CGFloat(numberOfPagesDisplayed) / 2 * pageDistance - pageDistance / 4 + alignmentOffset
}
/// Maximum offset allowed. This allows a bounce offset
var offsetUpperbound: CGFloat {
guard currentPage == numberOfPages - 1 else { return -CGFloat(numberOfPages) * self.size.width }
- return -CGFloat(numberOfPagesDisplayed) / 2 * pageDistance + pageDistance / 4
+ return -CGFloat(numberOfPagesDisplayed) / 2 * pageDistance + pageDistance / 4 + alignmentOffset
}
/// Addition of `draggingOffset` and `contentOffset`
@@ -107,12 +107,35 @@ extension Pager {
return min(numberOfPages, maximumNumberOfPages + page)
}
+ /// Extra offset to complentate the alignment
+ var alignmentOffset: CGFloat {
+ let offset: CGFloat
+ switch alignment {
+ case .center:
+ offset = 0
+ case .end(let insets):
+ if isVertical {
+ offset = (size.height - pageSize.height) / 2 - insets
+ } else {
+ offset = (size.width - pageSize.width) / 2 - insets
+ }
+ case .start(let insets):
+ if isVertical {
+ offset = -(size.height - pageSize.height) / 2 + insets
+ } else {
+ offset = -(size.width - pageSize.width) / 2 + insets
+ }
+ }
+
+ return offset
+ }
+
/// Offset applied to `HStack`. It's limitted by `offsetUpperbound` and `offsetUpperbound`
var xOffset: CGFloat {
let page = CGFloat(self.page - lowerPageDisplayed)
let numberOfPages = CGFloat(numberOfPagesDisplayed)
let xIncrement = pageDistance / 2
- let offset = (numberOfPages / 2 - page) * pageDistance - xIncrement + totalOffset
+ let offset = (numberOfPages / 2 - page) * pageDistance - xIncrement + totalOffset + alignmentOffset
return max(offsetUpperbound, min(offsetLowerbound, offset))
}
@@ -120,27 +143,27 @@ extension Pager {
func angle(for item: Element) -> Angle {
guard shouldRotate else { return .zero }
guard let index = data.firstIndex(of: item) else { return .zero }
-
+
let totalIncrement = abs(totalOffset / pageDistance)
-
+
let currentAngle = index == page ? .zero : index < page ? Angle(degrees: rotationDegrees) : Angle(degrees: -rotationDegrees)
guard isDragging else {
return currentAngle
}
-
+
let newAngle = direction == .forward ? Angle(degrees: currentAngle.degrees + rotationDegrees * Double(totalIncrement)) : Angle(degrees: currentAngle.degrees - rotationDegrees * Double(totalIncrement) )
return newAngle
}
-
+
/// Axis for the rotations effect
func axis(for item: Element) -> (CGFloat, CGFloat, CGFloat) {
guard shouldRotate else { return (0, 0, 0) }
guard let index = data.firstIndex(of: item) else { return (0, 0, 0) }
-
+
let currentXAxis: CGFloat = index == page ? 0 : index < page ? rotationAxis.x : -rotationAxis.x
return (currentXAxis, rotationAxis.y, rotationAxis.z)
}
-
+
/// Scale that applies to a particular item
func scale(for item: Element) -> CGFloat {
guard isDragging else { return isFocused(item) ? 1 : interactiveScale }
diff --git a/Sources/SwiftUIPager/Pager.swift b/Sources/SwiftUIPager/Pager.swift
index 81d840e..9bf7836 100644
--- a/Sources/SwiftUIPager/Pager.swift
+++ b/Sources/SwiftUIPager/Pager.swift
@@ -29,7 +29,7 @@ import SwiftUI
/// - 30 px of vertical insets
/// - 0.6 shrink ratio for items that aren't focused.
///
-public struct Pager: View where PageView: View, Element: Identifiable & Equatable {
+public struct Pager: View where PageView: View, Element: Equatable, ID: Hashable {
/// `Direction` determines the direction of the swipe gesture
enum Direction {
@@ -39,41 +39,77 @@ public struct Pager: View where PageView: View, Element: Ide
case backward
}
+ /// `Alignment` determines the focused page alignment inside `Pager`
+ public enum Alignment {
+ /// Sets the alignment to be centered
+ case center
+
+ /// Sets the alignment to be at the start of the container with the specified insets:
+ ///
+ /// - Left, if horizontal
+ /// - Top, if vertical
+ case start(CGFloat)
+
+ /// Sets the alignment to be at the start of the container with the specified insets:
+ ///
+ /// - Right, if horizontal
+ /// - Bottom, if vertical
+ case end(CGFloat)
+
+ /// Sets the alignment at the start, with 0 px of margin
+ public static var start: Alignment { .start(0) }
+
+ /// Sets the alignment at the end, with 0 px of margin
+ public static var end: Alignment { .end(0) }
+ }
+
/*** Constants ***/
/// Manages the number of items that should be displayed in the screen.
/// A ratio of 3, for instance, would mean the items held in memory are enough
/// to cover 3 times the size of the pager
let recyclingRatio = 4
-
+
/// Angle of rotation when should rotate
let rotationDegrees: Double = 20
-
+
/// Angle of rotation when should rotate
let rotationInteractiveScale: CGFloat = 0.7
-
+
/// Axis of rotation when should rotate
let rotationAxis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0, 1, 0)
/*** Dependencies ***/
-
+
+ /// Angle to dermine the direction of the scroll
+ var scrollDirectionAngle: Angle = .zero
+
/// `ViewBuilder` block to create each page
let content: (Element) -> PageView
+ /// `KeyPath` to data id property
+ let id: KeyPath
+
/// Array of items that will populate each page
var data: [Element]
/*** ViewModified properties ***/
+ /// The elements alignment relative to the container
+ var alignment: Alignment = .center
+
/// `true` if items are tapable
var isItemTappable: Bool = false
+ /// `true` if items user interaction is enabled
+ var isUserInteractionEnabled: Bool = true
+
/// `true` if the pager is horizontal
var isHorizontal: Bool = true
/// Shrink ratio that affects the items that aren't focused
var interactiveScale: CGFloat = 1
-
+
/// `true` if pages should have a 3D rotation effect
var shouldRotate: Bool = false
@@ -91,7 +127,7 @@ public struct Pager: View where PageView: View, Element: Ide
/// Callback for every new page
var onPageChanged: ((Int) -> Void)?
-
+
/*** State and Binding properties ***/
/// Size of the view
@@ -114,35 +150,54 @@ public struct Pager: View where PageView: View, Element: Ide
///
/// - Parameter page: Binding to the index of the focused page
/// - Parameter data: Array of items to populate the content
+ /// - Parameter id: KeyPath to identifiable property
/// - Parameter content: Factory method to build new pages
- public init(page: Binding, data: [Element], @ViewBuilder content: @escaping (Element) -> PageView) {
+ public init(page: Binding, data: [Element], id: KeyPath, @ViewBuilder content: @escaping (Element) -> PageView) {
self._page = page
self.data = data
+ self.id = id
self.content = content
}
public var body: some View {
- HStack(spacing: self.interactiveItemSpacing) {
- ForEach(self.dataDisplayed) { item in
+ HStack(spacing: interactiveItemSpacing) {
+ ForEach(dataDisplayed, id: id) { item in
self.content(item)
.frame(size: self.pageSize)
.scaleEffect(self.scale(for: item))
- .rotation3DEffect(self.isHorizontal ? .zero : Angle(degrees: -90),
+ .rotation3DEffect((self.isHorizontal ? .zero : Angle(degrees: -90)) - self.scrollDirectionAngle,
axis: (0, 0, 1))
.rotation3DEffect(self.angle(for: item),
axis: self.axis(for: item))
.gesture(self.tapGesture(for: item))
- .disabled(self.isFocused(item) || !self.isItemTappable)
+ .disabled(self.isFocused(item) || !self.isItemTappable || !self.isUserInteractionEnabled)
}
.offset(x: self.xOffset, y : 0)
}
- .gesture(self.swipeGesture)
- .rotation3DEffect(isHorizontal ? .zero : Angle(degrees: 90),
+ .gesture(swipeGesture)
+ .disabled(!isUserInteractionEnabled)
+ .rotation3DEffect((isHorizontal ? .zero : Angle(degrees: 90)) + scrollDirectionAngle,
axis: (0, 0, 1))
.sizeTrackable($size)
}
}
+extension Pager where ID == Element.ID, Element : Identifiable {
+
+ /// Initializes a new Pager.
+ ///
+ /// - Parameter page: Binding to the index of the focused page
+ /// - Parameter data: Array of items to populate the content
+ /// - Parameter content: Factory method to build new pages
+ public init(page: Binding, data: [Element], @ViewBuilder content: @escaping (Element) -> PageView) {
+ self._page = page
+ self.data = data
+ self.id = \Element.id
+ self.content = content
+ }
+
+}
+
// MARK: Gestures
extension Pager {
@@ -174,7 +229,7 @@ extension Pager {
let velocity = -Double(value.translation.width) / value.time.timeIntervalSince(self.draggingStartTime ?? Date())
var newPage = self.currentPage
if newPage == self.page, abs(velocity) > 1000 {
- newPage = newPage + Int(velocity / velocity)
+ newPage = newPage + Int(velocity / abs(velocity))
}
newPage = max(0, min(self.numberOfPages - 1, newPage))
withAnimation(.easeOut) {
diff --git a/SwiftUIPager.podspec b/SwiftUIPager.podspec
index 806c24d..3a246af 100644
--- a/SwiftUIPager.podspec
+++ b/SwiftUIPager.podspec
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "SwiftUIPager"
- s.version = "1.1.1"
+ s.version = "1.2.0"
s.summary = "Native pager for SwiftUI. Easily to use, easy to customize."
s.description = <<-DESC
diff --git a/resources/orientation-alignment.gif b/resources/orientation-alignment.gif
new file mode 100644
index 0000000..8d3bd52
Binary files /dev/null and b/resources/orientation-alignment.gif differ