├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Resources
└── Demo.gif
└── Sources
└── SwiftUIPageView
├── API
├── HPageView.swift
├── HorizontalPageAlignment.swift
├── PageAlignment.swift
├── PageView.swift
├── PageViewProxy.swift
├── PageViewReader.swift
├── VPageView.swift
├── VerticalPageAlignment.swift
└── View+.swift
└── Internal
├── Animation+.swift
├── AnimationState.swift
├── CGFloat+.swift
├── DragAnimator.swift
├── DragGesture+.swift
├── EnvironmentValues+.swift
├── HorizontalPageAlignment+.swift
├── InteractionProxy.swift
├── InteractionProxyKey.swift
├── PageAlignment+.swift
├── PageGestureView.swift
├── PageLayoutView.swift
├── PageState.swift
├── PageView+.swift
├── VerticalPageAlignment+.swift
└── ViewCounter.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Ciaran O'Brien
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SwiftUIPageView",
7 | platforms: [
8 | .iOS(.v14),
9 | .watchOS(.v7)
10 | ],
11 | products: [
12 | .library(
13 | name: "SwiftUIPageView",
14 | targets: ["SwiftUIPageView"]),
15 | ],
16 | dependencies: [
17 | ],
18 | targets: [
19 | .target(
20 | name: "SwiftUIPageView",
21 | dependencies: [])
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI PageView
2 |
3 | SwiftUI stack views with paged scrolling behaviour.
4 |
5 | > [!WARNING]
6 | > This package is no longer maintained. Use ScrollView and [scrollTargetBehaviour](https://developer.apple.com/documentation/swiftui/view/scrolltargetbehavior(_:)) instead.
7 |
8 | ## HPageView
9 | A view that arranges its children in a horizontal line, and provides paged scrolling behaviour.
10 |
11 | ### Usage
12 | ```swift
13 | HPageView(alignment: .leading, pageWidth: 250, spacing: 12) {
14 | //Pages
15 | }
16 | ```
17 |
18 | ### Parameters
19 | * `alignment`: The guide for aligning the pages in this page view.
20 | * `pageWidth`: The width of each page, or `nil` if you want each page to fill the width of the page view.
21 | * `spacing`: The distance between adjacent pages, or `nil` if you want the page view to choose a default distance for each pair of pages.
22 | * `content`: A view builder that creates the content of this page view.
23 |
24 | ## VPageView
25 | A view that arranges its children in a vertical line, and provides paged scrolling behaviour.
26 |
27 | ### Usage
28 | ```swift
29 | VPageView(alignment: .top, pageHeight: 250, spacing: 12) {
30 | //Pages
31 | }
32 | ```
33 |
34 | ### Parameters
35 | * `alignment`: The guide for aligning the pages in this page view.
36 | * `pageHeight`: The height of each page, or `nil` if you want each page to fill the height of the page view.
37 | * `spacing`: The distance between adjacent pages, or `nil` if you want the page view to choose a default distance for each pair of pages.
38 | * `content`: A view builder that creates the content of this page view.
39 |
40 | ## PageView
41 | A view that arranges its children in a line, and provides paged scrolling behaviour.
42 |
43 | **Changes to the layout axis will cause the pages to lose any internal state, and will not be animated.**
44 |
45 | ### Usage
46 | ```swift
47 | PageView(.horizontal, alignment: .leading, pageLength: 250, spacing: 12) {
48 | //Pages
49 | }
50 | ```
51 |
52 | ### Parameters
53 | * `axis`: The layout axis of this page view.
54 | * `alignment`: The guide for aligning the pages in this page view.
55 | * `pageLength`: The length of each page, parallel to the layout axis, or `nil` if you want each page to fill the length of the page view.
56 | * `spacing`: The distance between adjacent pages, or `nil` if you want the page view to choose a default distance for each pair of pages.
57 | * `content`: A view builder that creates the content of this page view.
58 |
59 | ## PageViewReader
60 | A view that provides programmatic paging, by working with a proxy to move to child pages.
61 |
62 | ### Usage
63 | ```swift
64 | PageViewReader { proxy in
65 | HPageView {
66 | //Pages
67 |
68 | Button("First") {
69 | withAnimation {
70 | proxy.moveToFirst()
71 | }
72 | }
73 | }
74 | }
75 | ```
76 |
77 | ### Parameters
78 | * `content`: The reader's content, containing a page view.
79 |
80 | ## PageViewProxy
81 | A proxy value that supports programmatic paging of the first page view within a view hierarchy.
82 |
83 | ### Functions
84 | * `moveTo(index:)`: Scans the first page view contained by the proxy for the page with the index closest to `index`, and then moves to that page.
85 | * `moveToFirst()`: Scans the first page view contained by the proxy for the first page, and then moves to that page.
86 | * `moveToLast()`: Scans the first page view contained by the proxy for the last page, and then moves to that page.
87 |
88 | ## Advanced Usage
89 | The `strictPageAlignment` view modifier can be used to control whether page views always use their provided alignment to position pages. Without this modifier pages will be aligned to prevent leaving empty space in the page view.
90 |
91 | ## Known Issues
92 | * Changes to the layout axis of a `PageView` will cause the pages to lose any internal state, and will not be animated.
93 | * Active paging animations in a page view may interfere with other animations when the number of pages changes.
94 | * Nested page views are not currently supported.
95 |
96 | ## Requirements
97 |
98 | * iOS 14.0+ or watchOS 7.0+
99 | * Xcode 12.0+
100 |
101 | ## Installation
102 |
103 | * Install with [Swift Package Manager](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
104 | * Import `SwiftUIPageView` to start using.
105 |
106 | ## Contact
107 |
108 | [@ciaranrobrien](https://twitter.com/ciaranrobrien) on Twitter.
109 |
--------------------------------------------------------------------------------
/Resources/Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciaranrobrien/SwiftUIPageView/738e2498b21651165ac9731e3906b8793c378995/Resources/Demo.gif
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/HPageView.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// A view that arranges its children in a horizontal line, and provides
10 | /// paged scrolling behaviour.
11 | public struct HPageView: View
12 | where Content : View
13 | {
14 | public var body: PageView
15 |
16 | /// A view that arranges its children in a horizontal line, and provides
17 | /// paged scrolling behaviour.
18 | ///
19 | /// This view returns a flexible preferred size to its parent layout.
20 | ///
21 | /// - Parameters:
22 | /// - alignment: The guide for aligning the pages in this page view.
23 | /// - pageWidth: The width of each page, or `nil` if you want each
24 | /// page to fill the width of the page view.
25 | /// - spacing: The distance between adjacent pages, or `nil` if you
26 | /// want the page view to choose a default distance for each pair of
27 | /// pages.
28 | /// - content: A view builder that creates the content of this page view.
29 | public init(alignment: PageAlignment = .center,
30 | pageWidth: CGFloat? = nil,
31 | spacing: CGFloat? = nil,
32 | @ViewBuilder content: @escaping () -> Content)
33 | {
34 | body = PageView(alignment: alignment.alignment,
35 | axis: .horizontal,
36 | content: content,
37 | pageLength: pageWidth,
38 | spacing: spacing)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/HorizontalPageAlignment.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | /// An alignment position along the horizontal axis.
8 | public enum HorizontalPageAlignment: CaseIterable, Hashable {
9 |
10 | /// A guide marking the leading edge of the page.
11 | case leading
12 |
13 | /// A guide marking the horizontal center of the page.
14 | case center
15 |
16 | /// A guide marking the trailing edge of the page.
17 | case trailing
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/PageAlignment.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// An alignment in both axes.
10 | public struct PageAlignment: Equatable
11 | where HAlignment : Equatable,
12 | VAlignment : Equatable
13 | {
14 |
15 | /// The alignment on the horizontal axis.
16 | public var horizontal: HAlignment
17 |
18 | /// The alignment on the vertical axis.
19 | public var vertical: VAlignment
20 |
21 | private init() {
22 | fatalError("PageAlignment cannot be initialized using private init().")
23 | }
24 | }
25 |
26 |
27 | public extension PageAlignment
28 | where HAlignment == HorizontalPageAlignment,
29 | VAlignment == VerticalAlignment
30 | {
31 |
32 | /// Creates an instance with the given horizontal and vertical alignments.
33 | ///
34 | /// - Parameters:
35 | /// - horizontal: The alignment on the horizontal axis.
36 | /// - vertical: The alignment on the vertical axis.
37 | init(horizontal: HAlignment, vertical: VAlignment) {
38 | self.horizontal = horizontal
39 | self.vertical = vertical
40 | }
41 |
42 | /// A guide marking the top and leading edges of the page.
43 | static let topLeading = PageAlignment(horizontal: .leading, vertical: .top)
44 |
45 | /// A guide marking the top edge of the page.
46 | static let top = PageAlignment(horizontal: .center, vertical: .top)
47 |
48 | /// A guide marking the top and trailing edges of the page.
49 | static let topTrailing = PageAlignment(horizontal: .trailing, vertical: .top)
50 |
51 | /// A guide marking the leading edge of the page.
52 | static let leading = PageAlignment(horizontal: .leading, vertical: .center)
53 |
54 | /// A guide marking the center of the page.
55 | static let center = PageAlignment(horizontal: .center, vertical: .center)
56 |
57 | /// A guide marking the trailing edge of the page.
58 | static let trailing = PageAlignment(horizontal: .trailing, vertical: .center)
59 |
60 | /// A guide marking the bottom and leading edges of the page.
61 | static let bottomLeading = PageAlignment(horizontal: .leading, vertical: .bottom)
62 |
63 | /// A guide marking the bottom edge of the page.
64 | static let bottom = PageAlignment(horizontal: .center, vertical: .bottom)
65 |
66 | /// A guide marking the bottom and trailing edges of the page.
67 | static let bottomTrailing = PageAlignment(horizontal: .trailing, vertical: .bottom)
68 | }
69 |
70 |
71 | public extension PageAlignment
72 | where HAlignment == HorizontalAlignment,
73 | VAlignment == VerticalPageAlignment
74 | {
75 | /// Creates an instance with the given horizontal and vertical alignments.
76 | ///
77 | /// - Parameters:
78 | /// - horizontal: The alignment on the horizontal axis.
79 | /// - vertical: The alignment on the vertical axis.
80 | init(horizontal: HAlignment, vertical: VAlignment) {
81 | self.horizontal = horizontal
82 | self.vertical = vertical
83 | }
84 |
85 | /// A guide marking the top and leading edges of the page.
86 | static let topLeading = PageAlignment(horizontal: .leading, vertical: .top)
87 |
88 | /// A guide marking the top edge of the page.
89 | static let top = PageAlignment(horizontal: .center, vertical: .top)
90 |
91 | /// A guide marking the top and trailing edges of the page.
92 | static let topTrailing = PageAlignment(horizontal: .trailing, vertical: .top)
93 |
94 | /// A guide marking the leading edge of the page.
95 | static let leading = PageAlignment(horizontal: .leading, vertical: .center)
96 |
97 | /// A guide marking the center of the page.
98 | static let center = PageAlignment(horizontal: .center, vertical: .center)
99 |
100 | /// A guide marking the trailing edge of the page.
101 | static let trailing = PageAlignment(horizontal: .trailing, vertical: .center)
102 |
103 | /// A guide marking the bottom and leading edges of the page.
104 | static let bottomLeading = PageAlignment(horizontal: .leading, vertical: .bottom)
105 |
106 | /// A guide marking the bottom edge of the page.
107 | static let bottom = PageAlignment(horizontal: .center, vertical: .bottom)
108 |
109 | /// A guide marking the bottom and trailing edges of the page.
110 | static let bottomTrailing = PageAlignment(horizontal: .trailing, vertical: .bottom)
111 | }
112 |
113 |
114 | public extension PageAlignment
115 | where HAlignment == HorizontalPageAlignment,
116 | VAlignment == VerticalPageAlignment
117 | {
118 | /// Creates an instance with the given horizontal and vertical alignments.
119 | ///
120 | /// - Parameters:
121 | /// - horizontal: The alignment on the horizontal axis.
122 | /// - vertical: The alignment on the vertical axis.
123 | init(horizontal: HAlignment, vertical: VAlignment) {
124 | self.horizontal = horizontal
125 | self.vertical = vertical
126 | }
127 |
128 | /// A guide marking the top and leading edges of the page.
129 | static let topLeading = PageAlignment(horizontal: .leading, vertical: .top)
130 |
131 | /// A guide marking the top edge of the page.
132 | static let top = PageAlignment(horizontal: .center, vertical: .top)
133 |
134 | /// A guide marking the top and trailing edges of the page.
135 | static let topTrailing = PageAlignment(horizontal: .trailing, vertical: .top)
136 |
137 | /// A guide marking the leading edge of the page.
138 | static let leading = PageAlignment(horizontal: .leading, vertical: .center)
139 |
140 | /// A guide marking the center of the page.
141 | static let center = PageAlignment(horizontal: .center, vertical: .center)
142 |
143 | /// A guide marking the trailing edge of the page.
144 | static let trailing = PageAlignment(horizontal: .trailing, vertical: .center)
145 |
146 | /// A guide marking the bottom and leading edges of the page.
147 | static let bottomLeading = PageAlignment(horizontal: .leading, vertical: .bottom)
148 |
149 | /// A guide marking the bottom edge of the page.
150 | static let bottom = PageAlignment(horizontal: .center, vertical: .bottom)
151 |
152 | /// A guide marking the bottom and trailing edges of the page.
153 | static let bottomTrailing = PageAlignment(horizontal: .trailing, vertical: .bottom)
154 | }
155 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/PageView.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// A view that arranges its children in a line, and provides paged
10 | /// scrolling behaviour.
11 | public struct PageView: View
12 | where Content : View
13 | {
14 | @Environment(\.displayScale) internal var displayScale
15 |
16 | public var body: some View {
17 | GeometryReader { geometry in
18 | let spacing = spacing ?? 8
19 | let viewLength = viewLength(for: geometry)
20 | let pageLength = pageLength(viewLength: viewLength)
21 | let baseOffset = baseOffset(pageLength: pageLength, viewLength: viewLength)
22 |
23 | PageGestureView(alignment: alignment,
24 | axis: axis,
25 | baseOffset: baseOffset,
26 | content: content,
27 | pageLength: pageLength,
28 | spacing: spacing,
29 | viewLength: viewLength)
30 | }
31 | .animation(nil, value: axis)
32 | }
33 |
34 | internal var alignment: Alignment
35 | internal var axis: Axis
36 | internal var content: () -> Content
37 | internal var pageLength: CGFloat?
38 | internal var spacing: CGFloat?
39 | }
40 |
41 |
42 | public extension PageView {
43 |
44 | /// A view that arranges its children in a line, and provides paged
45 | /// scrolling behaviour.
46 | ///
47 | /// This view returns a flexible preferred size to its parent layout.
48 | ///
49 | /// Changes to the layout axis will cause the pages to lose any internal
50 | /// state, and will not be animated.
51 | ///
52 | /// - Parameters:
53 | /// - axis: The layout axis of this page view.
54 | /// - alignment: The guide for aligning the pages in this page view.
55 | /// - pageLength: The length of each page, parallel to the layout axis,
56 | /// or `nil` if you want each page to fill the length of the page view.
57 | /// - spacing: The distance between adjacent pages, or `nil` if you
58 | /// want the page view to choose a default distance for each pair of
59 | /// pages.
60 | /// - content: A view builder that creates the content of this page view.
61 | init(_ axis: Axis,
62 | alignment: PageAlignment = .center,
63 | pageLength: CGFloat? = nil,
64 | spacing: CGFloat? = nil,
65 | @ViewBuilder content: @escaping () -> Content)
66 | {
67 | self.alignment = alignment.alignment
68 | self.axis = axis
69 | self.content = content
70 | self.pageLength = pageLength
71 | self.spacing = spacing
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/PageViewProxy.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// A proxy value that supports programmatic paging of the first
10 | /// page view within a view hierarchy.
11 | public struct PageViewProxy {
12 |
13 | /// Scans the first page view contained by the proxy for the
14 | /// page with the index closest to `index`, and then moves
15 | /// to that page.
16 | ///
17 | /// - Parameters:
18 | /// - index: The index of the page to move to.
19 | public func moveTo(_ index: Int) {
20 | interactionProxy?.moveTo(CGFloat(index))
21 | }
22 |
23 | /// Scans the first page view contained by the proxy for the
24 | /// first page, and then moves to that page.
25 | public func moveToFirst() {
26 | interactionProxy?.moveTo(-.infinity)
27 | }
28 |
29 | /// Scans the first page view contained by the proxy for the
30 | /// last page, and then moves to that page.
31 | public func moveToLast() {
32 | interactionProxy?.moveTo(.infinity)
33 | }
34 |
35 | internal var interactionProxy: InteractionProxy? = nil
36 |
37 | internal init() { }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/PageViewReader.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// A view that provides programmatic paging, by working with a proxy
10 | /// to move to child pages.
11 | public struct PageViewReader: View
12 | where Content : View
13 | {
14 |
15 | /// Creates an instance that can perform programmatic paging of its
16 | /// child page views.
17 | ///
18 | /// - Parameters:
19 | /// - content: The reader's content, containing a page view.
20 | public init(@ViewBuilder content: @escaping (PageViewProxy) -> Content) {
21 | self.content = content
22 | }
23 |
24 | public var body: some View {
25 | content(proxy)
26 | .onPreferenceChange(InteractionProxyKey.self) { proxy.interactionProxy = $0 }
27 | .transformPreference(InteractionProxyKey.self) { $0 = InteractionProxyKey.defaultValue }
28 | }
29 |
30 | @State private var proxy = PageViewProxy()
31 | private var content: (PageViewProxy) -> Content
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/VPageView.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// A view that arranges its children in a vertical line, and provides
10 | /// paged scrolling behaviour.
11 | public struct VPageView: View
12 | where Content : View
13 | {
14 | public var body: PageView
15 |
16 | /// A view that arranges its children in a vertical line, and provides
17 | /// paged scrolling behaviour.
18 | ///
19 | /// This view returns a flexible preferred size to its parent layout.
20 | ///
21 | /// - Parameters:
22 | /// - alignment: The guide for aligning the pages in this page view.
23 | /// - pageHeight: The height of each page, or `nil` if you want each
24 | /// page to fill the height of the page view.
25 | /// - spacing: The distance between adjacent pages, or `nil` if you
26 | /// want the page view to choose a default distance for each pair of
27 | /// pages.
28 | /// - content: A view builder that creates the content of this page view.
29 | public init(alignment: PageAlignment = .center,
30 | pageHeight: CGFloat? = nil,
31 | spacing: CGFloat? = nil,
32 | @ViewBuilder content: @escaping () -> Content)
33 | {
34 | body = PageView(alignment: alignment.alignment,
35 | axis: .vertical,
36 | content: content,
37 | pageLength: pageHeight,
38 | spacing: spacing)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/VerticalPageAlignment.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | /// An alignment position along the vertical axis.
8 | public enum VerticalPageAlignment: CaseIterable, Hashable {
9 |
10 | /// A guide marking the top edge of the page.
11 | case top
12 |
13 | /// A guide marking the vertical center of the page.
14 | case center
15 |
16 | /// A guide marking the bottom edge of the page.
17 | case bottom
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/API/View+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | public extension View {
10 |
11 | /// Adds a condition that controls whether page views always use
12 | /// their provided alignment to position pages.
13 | ///
14 | /// - Parameters:
15 | /// - strict: A Boolean value that determines whether page
16 | /// views always use their provided alignment to position pages.
17 | func strictPageAlignment(_ strict: Bool = true) -> some View {
18 | environment(\.strictPageAlignment, strict)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/Animation+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension Animation {
10 | static func dragEnded(distance: CGFloat, velocity: CGFloat, viewLength: CGFloat) -> Animation {
11 | let mass: CGFloat = 1
12 | let stiffness: CGFloat = 250
13 | let dampingRatio: CGFloat = 1
14 |
15 | let damping = 2 * dampingRatio * sqrt(mass * stiffness)
16 | let initialVelocity: CGFloat
17 |
18 | if abs(velocity) < .velocityThreshold {
19 | initialVelocity = sqrt(abs(distance * 200 / viewLength))
20 | } else if distance == 0 {
21 | initialVelocity = 0
22 | } else {
23 | initialVelocity = velocity / distance
24 | }
25 |
26 | return .interpolatingSpring(mass: mass,
27 | stiffness: stiffness,
28 | damping: damping,
29 | initialVelocity: initialVelocity)
30 | }
31 |
32 | static let dragStarted = Animation.linear(duration: 0)
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/AnimationState.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal class AnimationState: ObservableObject {
10 | var dragAnimation: Animation? = nil
11 | var viewAnimation: Animation? = nil
12 | var viewAnimationCanUpdate = true
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/CGFloat+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension CGFloat {
10 | static let velocityThreshold: CGFloat = 200
11 |
12 | func rubberBand(viewLength: CGFloat) -> CGFloat {
13 | (1 - (1 / ((magnitude * 0.55 / viewLength) + 1))) * viewLength * self / magnitude
14 | }
15 | func invertRubberBand(viewLength: CGFloat) -> CGFloat {
16 | (((1 / (1 - (magnitude / viewLength))) - 1) * viewLength / 0.55) * self / magnitude
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/DragAnimator.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct DragAnimator: AnimatableModifier {
10 | @State private var workItem: DispatchWorkItem? = nil
11 |
12 | var animatableData: CGFloat
13 | var computedOffset: CGFloat
14 | var pageState: PageState
15 |
16 | init(computedOffset: CGFloat, pageState: PageState) {
17 | self.animatableData = computedOffset
18 | self.computedOffset = computedOffset
19 | self.pageState = pageState
20 | }
21 |
22 | func body(content: Content) -> some View {
23 | content
24 | .onChange(of: animatableData) { animatableData in
25 | if workItem != nil {
26 | workItem?.cancel()
27 | workItem = nil
28 | }
29 |
30 | switch pageState.dragState {
31 | case .dragging, .ended:
32 | return
33 |
34 | case .ending, .nearlyEnded:
35 | pageState.offset = animatableData
36 |
37 | if animatableData == computedOffset {
38 | let computedOffset = computedOffset
39 | let workItem = DispatchWorkItem {
40 | if computedOffset == self.computedOffset {
41 | pageState.dragState = .ended
42 | }
43 | }
44 |
45 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
46 | self.workItem = workItem
47 | }
48 | else if abs(animatableData - computedOffset) < 2.5 {
49 | if pageState.dragState != .nearlyEnded {
50 | pageState.dragState = .nearlyEnded
51 | }
52 | }
53 | else {
54 | if pageState.dragState != .ending {
55 | pageState.dragState = .ending
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/DragGesture+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension DragGesture.Value {
10 | var velocity: CGSize {
11 | CGSize(width: (predictedEndTranslation.width - translation.width) * 4,
12 | height: (predictedEndTranslation.height - translation.height) * 4)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/EnvironmentValues+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension EnvironmentValues {
10 | var strictPageAlignment: Bool {
11 | get { self[StrictPageAlignmentKey.self] }
12 | set { self[StrictPageAlignmentKey.self] = newValue }
13 | }
14 | }
15 |
16 |
17 | private struct StrictPageAlignmentKey: EnvironmentKey {
18 | static let defaultValue = false
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/HorizontalPageAlignment+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension HorizontalPageAlignment {
10 | var alignment: HorizontalAlignment {
11 | switch self {
12 | case .leading: return .leading
13 | case .center: return .center
14 | case .trailing: return .trailing
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/InteractionProxy.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct InteractionProxy: Equatable {
10 | let id: UUID
11 | let moveTo: (CGFloat) -> Void
12 |
13 | static func == (lhs: InteractionProxy, rhs: InteractionProxy) -> Bool {
14 | lhs.id == rhs.id
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/InteractionProxyKey.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct InteractionProxyKey: PreferenceKey {
10 | static let defaultValue: InteractionProxy? = nil
11 |
12 | static func reduce(value: inout InteractionProxy?, nextValue: () -> InteractionProxy?) {
13 | value = nextValue()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/PageAlignment+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension PageAlignment {
10 | var alignment: Alignment {
11 | if let horizontal = horizontal as? HorizontalAlignment,
12 | let vertical = vertical as? VerticalPageAlignment
13 | {
14 | return Alignment(horizontal: horizontal, vertical: vertical.alignment)
15 | }
16 | else if let horizontal = horizontal as? HorizontalPageAlignment,
17 | let vertical = vertical as? VerticalAlignment
18 | {
19 | return Alignment(horizontal: horizontal.alignment, vertical: vertical)
20 | }
21 | else if let horizontal = horizontal as? HorizontalPageAlignment,
22 | let vertical = vertical as? VerticalPageAlignment
23 | {
24 | return Alignment(horizontal: horizontal.alignment, vertical: vertical.alignment)
25 | }
26 | else
27 | {
28 | fatalError("PageAlignment cannot compute Alignment.")
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/PageGestureView.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct PageGestureView: View
10 | where Content : View
11 | {
12 | @Environment(\.strictPageAlignment) private var strictPageAlignment
13 | @GestureState private var isDragging = false
14 | @StateObject private var animationState = AnimationState()
15 | @StateObject private var pageState = PageState()
16 |
17 | var alignment: Alignment
18 | var axis: Axis
19 | var baseOffset: CGFloat
20 | var content: () -> Content
21 | var pageLength: CGFloat
22 | var spacing: CGFloat
23 | var viewLength: CGFloat
24 |
25 | var body: some View {
26 | PageLayoutView(alignment: alignment,
27 | animationState: animationState,
28 | axis: axis,
29 | content: content,
30 | pageLength: pageLength,
31 | pageState: pageState,
32 | spacing: spacing,
33 | viewLength: viewLength)
34 | .modifier(DragAnimator(computedOffset: computedOffset, pageState: pageState))
35 | .offset(offset)
36 | .contentShape(Rectangle())
37 | .highPriorityGesture(gesture)
38 | .preference(key: InteractionProxyKey.self, value: InteractionProxy(id: pageState.id, moveTo: pageTo))
39 | .onChange(of: isCancelled, perform: onDragCancelled)
40 | }
41 |
42 | private var computedOffset: CGFloat {
43 | let computed = min(max(indexToOffset(pageState.index), offsetRange.lowerBound), offsetRange.upperBound)
44 | + indexToOffset(pageState.indexOffset)
45 |
46 | if computed > offsetRange.upperBound {
47 | return (computed - offsetRange.upperBound).rubberBand(viewLength: viewLength) + offsetRange.upperBound
48 | } else if computed < offsetRange.lowerBound {
49 | return (computed - offsetRange.lowerBound).rubberBand(viewLength: viewLength) + offsetRange.lowerBound
50 | } else {
51 | return computed
52 | }
53 | }
54 | private var gesture: some Gesture {
55 | let minimumDistance: CGFloat
56 |
57 | switch pageState.dragState {
58 | case .dragging, .nearlyEnded, .ended: minimumDistance = 15
59 | case .ending: minimumDistance = 0
60 | }
61 |
62 | return DragGesture(minimumDistance: minimumDistance)
63 | .onChanged(onDragChanged)
64 | .onEnded(onDragEnded)
65 | .updating($isDragging) { _, s, _ in s = true }
66 | }
67 | private var indexRange: ClosedRange {
68 | offsetToIndex(offsetRange.upperBound)...offsetToIndex(offsetRange.lowerBound)
69 | }
70 | private var isCancelled: Bool {
71 | switch pageState.dragState {
72 | case .dragging: return !isDragging
73 | case .ending, .nearlyEnded, .ended: return false
74 | }
75 | }
76 | private var offset: CGSize {
77 | switch axis {
78 | case .horizontal: return CGSize(width: baseOffset + computedOffset, height: 0)
79 | case .vertical: return CGSize(width: 0, height: baseOffset + computedOffset)
80 | }
81 | }
82 | private var offsetRange: ClosedRange {
83 | guard pageState.viewCount > 1
84 | else { return 0...0 }
85 |
86 | var lowerBound = -(CGFloat(pageState.viewCount - 1) * (pageLength + spacing))
87 | var upperBound: CGFloat = 0
88 |
89 | if !strictPageAlignment {
90 | upperBound = -baseOffset
91 | lowerBound += (viewLength - pageLength) - baseOffset
92 | }
93 |
94 | return lowerBound...upperBound
95 | }
96 |
97 | private func indexToOffset(_ index: CGFloat) -> CGFloat {
98 | -index * (pageLength + spacing)
99 | }
100 | private func offsetToIndex(_ offset: CGFloat) -> CGFloat {
101 | -offset / (pageLength + spacing)
102 | }
103 | private func onDragChanged(value: DragGesture.Value) {
104 | if let initialIndex = pageState.initialIndex {
105 | onDragUpdated(value: value, initialIndex: initialIndex)
106 | } else {
107 | onDragStarted(value: value)
108 | }
109 | }
110 | private func onDragCancelled(isCancelled: Bool) {
111 | guard isCancelled else { return }
112 |
113 | DispatchQueue.main.async {
114 | guard self.isCancelled else { return }
115 |
116 | let index = min(max(pageState.index, indexRange.lowerBound), indexRange.upperBound)
117 | var newIndex = round(index)
118 |
119 | if newIndex <= indexRange.lowerBound {
120 | newIndex = -.infinity
121 | } else if newIndex >= indexRange.upperBound {
122 | newIndex = .infinity
123 | }
124 |
125 | let distance = min(max(indexToOffset(newIndex), offsetRange.lowerBound), offsetRange.upperBound) - computedOffset
126 |
127 | animationState.dragAnimation = .dragEnded(distance: distance, velocity: 0, viewLength: viewLength)
128 | pageState.dragState = distance == 0 ? .ended : .ending
129 | pageState.initialIndex = nil
130 |
131 | withAnimation(animationState.dragAnimation) {
132 | pageState.index = newIndex
133 | pageState.indexOffset = 0
134 | }
135 | }
136 | }
137 | private func onDragEnded(value: DragGesture.Value) {
138 | let index = min(max(pageState.index + pageState.indexOffset, indexRange.lowerBound), indexRange.upperBound)
139 | var newIndex: CGFloat
140 | let velocity: CGFloat
141 |
142 | switch axis {
143 | case .horizontal: velocity = value.velocity.width
144 | case .vertical: velocity = value.velocity.height
145 | }
146 |
147 | if velocity <= -.velocityThreshold {
148 | newIndex = floor(index + 1)
149 | } else if velocity >= .velocityThreshold {
150 | newIndex = ceil(index - 1)
151 | } else {
152 | newIndex = round(index)
153 | }
154 |
155 | if newIndex <= indexRange.lowerBound {
156 | newIndex = -.infinity
157 | } else if newIndex >= indexRange.upperBound {
158 | newIndex = .infinity
159 | }
160 |
161 | let distance = min(max(indexToOffset(newIndex), offsetRange.lowerBound), offsetRange.upperBound) - computedOffset
162 |
163 | animationState.dragAnimation = .dragEnded(distance: distance, velocity: velocity, viewLength: viewLength)
164 | pageState.dragState = distance == 0 ? .ended : .ending
165 | pageState.initialIndex = nil
166 |
167 | withAnimation(animationState.dragAnimation) {
168 | pageState.index = newIndex
169 | pageState.indexOffset = 0
170 | }
171 | }
172 | private func onDragStarted(value: DragGesture.Value) {
173 | let additionalOffset: CGFloat
174 | let initialOffset: CGFloat
175 | var offset: CGFloat
176 |
177 | switch axis {
178 | case .horizontal: additionalOffset = value.translation.width
179 | case .vertical: additionalOffset = value.translation.height
180 | }
181 |
182 | switch pageState.dragState {
183 | case .dragging, .nearlyEnded, .ended: offset = computedOffset
184 | case .ending: offset = pageState.offset
185 | }
186 |
187 | if offset < offsetRange.lowerBound {
188 | initialOffset = additionalOffset - (offset - offsetRange.lowerBound).invertRubberBand(viewLength: viewLength)
189 | offset = offsetRange.lowerBound
190 | } else if offset > offsetRange.upperBound {
191 | initialOffset = additionalOffset - (offset - offsetRange.upperBound).invertRubberBand(viewLength: viewLength)
192 | offset = offsetRange.upperBound
193 | } else {
194 | initialOffset = additionalOffset
195 | }
196 |
197 | animationState.dragAnimation = .dragStarted
198 | pageState.dragState = .dragging
199 | pageState.initialIndex = offsetToIndex(initialOffset)
200 |
201 | withAnimation(animationState.dragAnimation) {
202 | pageState.index = offsetToIndex(offset)
203 | pageState.indexOffset = offsetToIndex(additionalOffset - initialOffset)
204 | }
205 | }
206 | private func onDragUpdated(value: DragGesture.Value, initialIndex: CGFloat) {
207 | let additionalOffset: CGFloat
208 |
209 | switch axis {
210 | case .horizontal: additionalOffset = value.translation.width
211 | case .vertical: additionalOffset = value.translation.height
212 | }
213 |
214 | pageState.indexOffset = offsetToIndex(additionalOffset) - initialIndex
215 | }
216 | private func pageTo(index: CGFloat) {
217 | let newIndex: CGFloat
218 |
219 | if index <= indexRange.lowerBound {
220 | newIndex = -.infinity
221 | } else if index >= indexRange.upperBound {
222 | newIndex = .infinity
223 | } else {
224 | newIndex = index
225 | }
226 |
227 | let distance = min(max(indexToOffset(newIndex), offsetRange.lowerBound), offsetRange.upperBound) - computedOffset
228 |
229 | pageState.dragState = distance == 0 ? .ended : .ending
230 | pageState.index = newIndex
231 | pageState.indexOffset = 0
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/PageLayoutView.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct PageLayoutView: View
10 | where Content : View
11 | {
12 | var alignment: Alignment
13 | var animationState: AnimationState
14 | var axis: Axis
15 | var content: () -> Content
16 | var pageLength: CGFloat
17 | var pageState: PageState
18 | var spacing: CGFloat
19 | var viewLength: CGFloat
20 |
21 | var body: some View {
22 | Group {
23 | switch axis {
24 | case .horizontal:
25 | HStack(alignment: alignment.vertical, spacing: spacing) {
26 | content()
27 | .frame(width: pageLength)
28 | }
29 | .modifier(ViewCounter(animationState: animationState,
30 | axis: axis,
31 | pageLength: pageLength,
32 | pageState: pageState,
33 | spacing: spacing))
34 | .frame(width: viewLength, alignment: .leading)
35 |
36 | case .vertical:
37 | VStack(alignment: alignment.horizontal, spacing: spacing) {
38 | content()
39 | .frame(height: pageLength)
40 | }
41 | .modifier(ViewCounter(animationState: animationState,
42 | axis: axis,
43 | pageLength: pageLength,
44 | pageState: pageState,
45 | spacing: spacing))
46 | .frame(height: viewLength, alignment: .top)
47 | }
48 | }
49 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/PageState.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal class PageState: ObservableObject {
10 | @Published var dragState = DragState.ended
11 | @Published var index: CGFloat = 0
12 | @Published var indexOffset: CGFloat = 0
13 | @Published var initialIndex: CGFloat? = nil
14 | var offset: CGFloat = 0
15 | @Published var viewCount = 0
16 |
17 | let id = UUID()
18 |
19 | enum DragState {
20 | case dragging
21 | case ending
22 | case nearlyEnded
23 | case ended
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/PageView+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension PageView {
10 | func baseOffset(pageLength: CGFloat, viewLength: CGFloat) -> CGFloat {
11 | switch axis {
12 | case .horizontal:
13 | switch alignment.horizontal {
14 | case .leading: return 0
15 | case .center: return (viewLength - pageLength) / 2
16 | case .trailing: return (viewLength - pageLength)
17 | default: fatalError("Unexpected HorizontalAlignment.")
18 | }
19 |
20 | case .vertical:
21 | switch alignment.vertical {
22 | case .top: return 0
23 | case .center: return (viewLength - pageLength) / 2
24 | case .bottom: return (viewLength - pageLength)
25 | default: fatalError("Unexpected VerticalAlignment.")
26 | }
27 | }
28 | }
29 | func pageLength(viewLength: CGFloat) -> CGFloat {
30 | max(round((pageLength ?? viewLength) * displayScale) / displayScale, 0)
31 | }
32 | func viewLength(for geometry: GeometryProxy) -> CGFloat {
33 | switch axis {
34 | case .horizontal: return geometry.size.width
35 | case .vertical: return geometry.size.height
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/VerticalPageAlignment+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension VerticalPageAlignment {
10 | var alignment: VerticalAlignment {
11 | switch self {
12 | case .top: return .top
13 | case .center: return .center
14 | case .bottom: return .bottom
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SwiftUIPageView/Internal/ViewCounter.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIPageView
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct ViewCounter: ViewModifier {
10 | var animationState: AnimationState
11 | var axis: Axis
12 | var pageLength: CGFloat
13 | var pageState: PageState
14 | var spacing: CGFloat
15 |
16 | func body(content: Content) -> some View {
17 | content
18 | .transaction { onAnimation($0.animation) }
19 | .background(
20 | GeometryReader { geometry in
21 | let state = ViewState(pageLength: pageLength, size: geometry.size, spacing: spacing)
22 |
23 | Color.clear
24 | .onAppear { onState(state: state) }
25 | .onChange(of: state, perform: onState)
26 | }
27 | .hidden()
28 | )
29 | }
30 |
31 | private func onAnimation(_ animation: Animation?) {
32 | if animation != animationState.dragAnimation && animationState.viewAnimationCanUpdate {
33 | animationState.viewAnimation = animation
34 | animationState.viewAnimationCanUpdate = false
35 |
36 | DispatchQueue.main.async {
37 | animationState.viewAnimationCanUpdate = true
38 | }
39 | }
40 | }
41 | private func onState(state: ViewState) {
42 | let count: Int
43 | let itemLength = state.pageLength + state.spacing
44 |
45 | if itemLength > 0 {
46 | let stackLength: CGFloat
47 |
48 | switch axis {
49 | case .horizontal: stackLength = state.size.width
50 | case .vertical: stackLength = state.size.height
51 | }
52 |
53 | count = Int(max(round((stackLength + state.spacing) / itemLength), 0))
54 | } else {
55 | count = 0
56 | }
57 |
58 | if pageState.viewCount != count {
59 | withAnimation(animationState.viewAnimation) {
60 | pageState.viewCount = count
61 | }
62 | }
63 | }
64 |
65 | private struct ViewState: Equatable {
66 | var pageLength: CGFloat
67 | var size: CGSize
68 | var spacing: CGFloat
69 | }
70 | }
71 |
--------------------------------------------------------------------------------