Skip to content

Commit

Permalink
Merge pull request #5 from fermoya/feat/adding-alignment
Browse files Browse the repository at this point in the history
Feat/adding alignment
  • Loading branch information
fermoya authored Jan 24, 2020
2 parents 0a1109a + 7103e78 commit 01a0879
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 83 deletions.
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img src="resources/example-of-usage.gif" alt="Example of usage"/>

## Requirements
Expand Down Expand Up @@ -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)
Expand All @@ -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(...)
Expand All @@ -72,9 +76,21 @@ Pager(...)

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

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)
```

<img src="resources/orientation-alignment.gif" alt="PageAspectRatio greater than 1" height="640"/>

### 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(...)
Expand All @@ -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:
Expand Down
16 changes: 8 additions & 8 deletions Sample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -32,7 +32,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6BB892E023D9F13C00AC9331 /* SwiftUIPager in Frameworks */,
6BEA272523DA3596001A2082 /* SwiftUIPager in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -94,7 +94,7 @@
);
name = Sample;
packageProductDependencies = (
6BB892DF23D9F13C00AC9331 /* SwiftUIPager */,
6BEA272423DA3596001A2082 /* SwiftUIPager */,
);
productName = SwiftUIPager;
productReference = 17D9E0F023D4CF6700C5AE93 /* Sample.app */;
Expand Down Expand Up @@ -125,7 +125,7 @@
);
mainGroup = 17D9E0E723D4CF6700C5AE93;
packageReferences = (
6BB892DE23D9F13C00AC9331 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
6BEA272323DA3596001A2082 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
);
productRefGroup = 17D9E0F123D4CF6700C5AE93 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
64 changes: 24 additions & 40 deletions Sample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 38 additions & 4 deletions Sources/SwiftUIPager/Pager+Buildable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 32 additions & 9 deletions Sources/SwiftUIPager/Pager+Helper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -107,40 +107,63 @@ 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))
}

/// Angle for the 3D rotation effect
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 }
Expand Down
Loading

0 comments on commit 01a0879

Please sign in to comment.