├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── INSTALL.md
├── Package.resolved
├── Package.swift
├── README.md
├── Resources
├── iPagesDemo1Dark.gif
├── iPagesDemo1Light.gif
├── iPagesDemo2Light.gif
├── ipages.gif
└── preview.gif
├── Sources
└── iPages
│ ├── Decomposable.swift
│ ├── PageControl.swift
│ ├── PageViewController+NSPageControllerDelegate.swift
│ ├── PageViewController+UIPageViewDelegate.swift
│ ├── PageViewController+UIScrollViewDelegate.swift
│ ├── PageViewController.swift
│ ├── Typealiases.swift
│ ├── iPages+ViewModifiers.swift
│ └── iPages.swift
├── Tests
├── LinuxMain.swift
└── iPagesTests
│ ├── XCTestManifests.swift
│ └── iPagesTests.swift
└── instructions
├── iPagesDemo2.gif
├── iPagesDemoPrimary.gif
├── instructions-1.png
├── instructions-2-iPages.png
└── instructions-3-iPages.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/INSTALL.md:
--------------------------------------------------------------------------------
1 | [*<<< Back to README*](https://github.com/benjaminsage/iPages)
2 |
3 |
4 | # Instructions
5 |
6 | ## **(1/3)** Open XCode. Go to [File > Swift Packges > Add Package Dependency...]
7 | 
8 |
9 | ## (2/3) Input https://github.com/benjaminsage/iPages.git & click Next.
10 | 
11 |
12 | ## (3/3) Select Version: Up to Next Major & click finish.
13 | 
14 |
15 |
16 |
17 | [*<<< Back to README*](https://github.com/benjaminsage/iPages)
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "iColor",
6 | "repositoryURL": "https://github.com/benjaminsage/iColor.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "586b4307cc9870dc170664a747b915c3fcd507c3",
10 | "version": "0.1.3"
11 | }
12 | },
13 | {
14 | "package": "iGraphics",
15 | "repositoryURL": "https://github.com/benjaminsage/iGraphics.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "2b0714f7fe333621365854ebdce44ea2db866405",
19 | "version": "0.0.7"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | import SwiftUI
6 |
7 | let package = Package(
8 | name: "iPages",
9 | platforms: [
10 | .iOS(.v13),
11 | .macOS(.v10_15)
12 | ],
13 | products: [
14 | // Products define the executables and libraries a package produces, and make them visible to other packages.
15 | .library(
16 | name: "iPages",
17 | targets: ["iPages"]),
18 | ],
19 | dependencies: [
20 | // Dependencies declare other packages that this package depends on.
21 | // .package(url: /* package url */, from: "1.0.0"),
22 | ],
23 | targets: [
24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
25 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
26 | .target(
27 | name: "iPages",
28 | dependencies: []),
29 | .testTarget(
30 | name: "iPagesTests",
31 | dependencies: ["iPages"]),
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
iPages📖
2 | Quickly implement swipable page views in iOS. 📝
3 |
4 |
5 | Get Started |
6 | Examples |
7 | Customize |
8 | Install |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## Get Started
17 |
18 | 1. [Install](https://github.com/benjaminsage/iPages/blob/main/INSTALL.md) `iPages`
19 |
20 | 2. Add `iPages` to your project
21 | ```swift
22 | import SwiftUI
23 | import iPages
24 |
25 | struct ContentView: View {
26 | var body: some View {
27 | iPages {
28 | Text("iPages 🤑")
29 | Color.pink
30 | }
31 | }
32 | }
33 | ```
34 |
35 | 3. Customize your `iPages`
36 |
37 |
38 | ## Examples
39 | ### Marketing Materials 💸
40 |
41 |
42 |
43 | Use `iGraphicsView` to demo marketing slides.
44 |
45 | ```swift
46 | import SwiftUI
47 | import iPages
48 | import iGraphics
49 |
50 | struct ContentView: View {
51 | var body: some View {
52 | iPages {
53 | iGraphicsView(.first)
54 | iGraphicsView(.second)
55 | iGraphicsView(.third)
56 | }
57 | }
58 | }
59 | ```
60 |
61 |
62 |
63 |
64 | Shopping App 🛍
65 | If you want, you can pass in your own optional selection binding to iPages. Hide the bottom dots & add infinite scroll to remove context.
66 |
67 |
68 |
69 | ```swift
70 | import SwiftUI
71 | import iPages
72 | import iGraphics
73 |
74 | struct ContentView: View {
75 | @State var currentPage: Int = 0
76 |
77 | var body: some View {
78 | iPages(selection: $currentPage) {
79 | iGraphicsBox(.photo)
80 | .stack(3)
81 | iGraphicsBox(.card)
82 | .stack(2)
83 | }
84 | .hideDots(true)
85 | .wraps(true)
86 | }
87 | }
88 | ```
89 |
90 |
91 |
92 |
93 |
94 | ## Customize 🎀
95 |
96 | `iPages` takes a trailing view builder of ordered views. You can also optionally pass in your own page index binding called `selection:`, to let you build your own page control, or however you want to use it. `iPages` supports a variety of custom modifiers. All customizations are built into our modifiers.
97 |
98 | **Example**: Change the dot colors, enable infinite wrap & hide dots for single page views with the following code block:
99 | ```swift
100 | iPages(selection: $currentPage) {
101 | Text("👏")
102 | }
103 | .dotsTintColors(currentPage: Color, otherPages: Color)
104 | .wraps(true)
105 | .dotsHideForSinglePage(true)
106 | .navigationOrientation(.vertical)
107 |
108 | ```
109 |
110 | Use our exhaustive input list to customize your views.
111 |
112 | | | Modifier or Initializer | Description
113 | | --- | --- | ---
114 | 👷♀️ | `.init(content:)` | Initializes the page 📃📖 view.
115 | 👷♂️ | `.init(selection:content:)` | Initializes the page 📃📖 view with a selection binding.
116 | ⏺ | `.hideDots(_:)` | Modifies whether or not the page view should include the standard page control dots. (••••)
117 | 🔄 | `.wraps(_:)` | Modifies whether or not the page view should restart at the beginning 🔁 when swiping past the end (and vise-versa)
118 | 1️⃣ | `.dotsHideForSinglePage(_:)` | Modifies whether the page dots are hidden when there is only one page. 1️⃣⤵️
119 | 🎨 | `.dotsTintColors(currentPage:otherPages:)` | Modifies tint colors 🟡🟢🔴🟣 to be used for the page dots.
120 | 🔘 | `.dotsBackgroundStyle(_:)` | Modifies the background style ⚪️🔘 of the page dots.
121 | 🔃 | `.dotsAllowContinuousInteraction(_:)` | Modifies the continuous interaction settings of the dots. 🔄
122 | ↔️ | `.dotsAlignment(_:)` | Modifies the **alignment of the page dots**. 👆 👇
123 | ↕️ | `.navigationOrientation(_:)` | Modifies the navigation **orientation** of the page view. ↔️ ↕️
124 | 🦿 | `.disableBounce(_:)` | Disables the **bounce** settings of the page view. This is especially useful for scroll views.
125 | ↔️ | `.interPageSpacing(_:)` | Modifies the spacing between the pages. ↔️
126 | 🎥 | `.animated(_:)` | Modifies whether the the pages animate the slide if the `selection` binding changes. 🎥
127 |
128 |
129 | ## Install
130 | Use the Swift package manager to install. Find instructions [here](https://github.com/benjaminsage/iPages/blob/main/INSTALL.md)😀
131 |
132 |
--------------------------------------------------------------------------------
/Resources/iPagesDemo1Dark.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/Resources/iPagesDemo1Dark.gif
--------------------------------------------------------------------------------
/Resources/iPagesDemo1Light.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/Resources/iPagesDemo1Light.gif
--------------------------------------------------------------------------------
/Resources/iPagesDemo2Light.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/Resources/iPagesDemo2Light.gif
--------------------------------------------------------------------------------
/Resources/ipages.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/Resources/ipages.gif
--------------------------------------------------------------------------------
/Resources/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/Resources/preview.gif
--------------------------------------------------------------------------------
/Sources/iPages/Decomposable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Decomposable.swift
3 | //
4 | //
5 | // Created by Benjamin Sage on 10/21/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | protocol Decomposable {
11 | func subviews() -> [AnyView]
12 | }
13 |
14 | extension View {
15 | static func any(from view: Any) -> AnyView {
16 | return AnyView(view as! Self)
17 | }
18 | }
19 |
20 | extension View {
21 | func decompose() -> [AnyView] {
22 | if let decomposable = self as? Decomposable {
23 | return decomposable.subviews()
24 | }
25 | return [AnyView(self)]
26 | }
27 | }
28 |
29 | extension ForEach: Decomposable where Content: View {
30 | func subviews() -> [AnyView] {
31 | return data.map(content).flatMap { $0.decompose() }
32 | }
33 | }
34 |
35 | extension TupleView: Decomposable {
36 | func subviews() -> [AnyView] {
37 | let mirror = Mirror(reflecting: self)
38 | let tuple = mirror.children.first!.value
39 | let tupleMirror = Mirror(reflecting: tuple)
40 | return tupleMirror.children.map { AnyView(_fromValue: $0.value)! }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/iPages/PageControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageControl.swift
3 | //
4 | //
5 | // Created by Benjamin Sage on 10/23/20.
6 | //
7 |
8 | #if os(iOS)
9 |
10 | import SwiftUI
11 | import UIKit
12 |
13 | struct PageControl: UIViewRepresentable {
14 | private var numberOfPages: Int
15 | @Binding private var currentPage: Int
16 |
17 | private var hidesForSinglePage: Bool = false
18 | private var pageIndicatorTintColor: UIColor?
19 | private var currentPageIndicatorTintColor: UIColor?
20 |
21 | private var _backgroundStyle: Any? = nil
22 | @available(iOS 14, *)
23 | private var backgroundStyle: UIPageControl.BackgroundStyle {
24 | get {
25 | if _backgroundStyle == nil {
26 | return .automatic
27 | } else {
28 | return _backgroundStyle as! UIPageControl.BackgroundStyle
29 | }
30 | }
31 | set(newStyle) {
32 | _backgroundStyle = newStyle
33 | }
34 | }
35 | private var allowsContinuousInteraction: Bool = true
36 |
37 | init(numberOfPages: Int,
38 | currentPage: Binding,
39 | hidesForSinglePage: Bool,
40 | pageIndicatorTintColor: UIColor?,
41 | currentPageIndicatorTintColor: UIColor?,
42 | allowsContinuousInteraction: Bool)
43 | {
44 | self.numberOfPages = numberOfPages
45 | self._currentPage = currentPage
46 | self.hidesForSinglePage = hidesForSinglePage
47 | self.pageIndicatorTintColor = pageIndicatorTintColor
48 | self.currentPageIndicatorTintColor = currentPageIndicatorTintColor
49 | self.allowsContinuousInteraction = allowsContinuousInteraction
50 | }
51 |
52 | @available(iOS 14.0, *)
53 | init(numberOfPages: Int,
54 | currentPage: Binding,
55 | hidesForSinglePage: Bool,
56 | pageIndicatorTintColor: UIColor?,
57 | currentPageIndicatorTintColor: UIColor?,
58 | backgroundStyle: UIPageControl.BackgroundStyle,
59 | allowsContinuousInteraction: Bool)
60 | {
61 | self.numberOfPages = numberOfPages
62 | self._currentPage = currentPage
63 | self.hidesForSinglePage = hidesForSinglePage
64 | self.pageIndicatorTintColor = pageIndicatorTintColor
65 | self.currentPageIndicatorTintColor = currentPageIndicatorTintColor
66 | self.backgroundStyle = backgroundStyle
67 | self.allowsContinuousInteraction = allowsContinuousInteraction
68 | }
69 |
70 | func makeCoordinator() -> Coordinator {
71 | Coordinator(self)
72 | }
73 |
74 | func makeUIView(context: Context) -> UIPageControl {
75 | let control = UIPageControl()
76 | control.numberOfPages = numberOfPages
77 | control.addTarget(
78 | context.coordinator,
79 | action: #selector(Coordinator.updateCurrentPage(sender:)),
80 | for: .valueChanged)
81 |
82 | control.hidesForSinglePage = hidesForSinglePage
83 | control.pageIndicatorTintColor = pageIndicatorTintColor
84 | control.currentPageIndicatorTintColor = currentPageIndicatorTintColor
85 |
86 | if #available(iOS 14, *) {
87 | control.backgroundStyle = backgroundStyle
88 | control.allowsContinuousInteraction = allowsContinuousInteraction
89 | }
90 |
91 | return control
92 | }
93 |
94 | func updateUIView(_ uiView: UIPageControl, context: Context) {
95 | uiView.currentPage = currentPage
96 | }
97 |
98 | class Coordinator: NSObject {
99 | var control: PageControl
100 |
101 | init(_ control: PageControl) {
102 | self.control = control
103 | }
104 |
105 | @objc func updateCurrentPage(sender: UIPageControl) {
106 | control.currentPage = sender.currentPage
107 | }
108 | }
109 | }
110 |
111 | #endif
112 |
--------------------------------------------------------------------------------
/Sources/iPages/PageViewController+NSPageControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageViewController+NSPageControllerDelegate.swift
3 | //
4 | //
5 | // Created by Benjamin Leonardo Sage on 11/19/20.
6 | //
7 |
8 | #if os(macOS)
9 |
10 | import SwiftUI
11 | import AppKit
12 |
13 | extension PageViewController {
14 | class Coordinator: NSObject, NSPageControllerDelegate {
15 | var parent: PageViewController
16 |
17 | init(_ pageViewController: PageViewController) {
18 | self.parent = pageViewController
19 | }
20 |
21 | func pageController(
22 | _ pageController: NSPageController,
23 | identifierFor object: Any) -> NSPageController.ObjectIdentifier
24 | {
25 | return NSPageController.ObjectIdentifier(describing: object)
26 | }
27 |
28 | func pageController(
29 | _ pageController: NSPageController,
30 | viewControllerForIdentifier identifier: NSPageController.ObjectIdentifier) -> NSViewController
31 | {
32 | return parent.controllers[Int(identifier) ?? -1]
33 | }
34 |
35 | func pageController(
36 | _ pageController: NSPageController,
37 | prepare viewController: NSViewController,
38 | with object: Any?)
39 | {
40 |
41 | }
42 |
43 | func pageControllerWillStartLiveTransition(_ pageController: NSPageController) {
44 | }
45 |
46 | func pageController(
47 | _ pageController: NSPageController,
48 | didTransitionTo object: Any)
49 | {
50 |
51 | }
52 |
53 | func pageControllerDidEndLiveTransition(_ pageController: NSPageController) {
54 | parent.currentPage = pageController.selectedIndex
55 | pageController.completeTransition()
56 | }
57 | }
58 | }
59 |
60 | #endif
61 |
--------------------------------------------------------------------------------
/Sources/iPages/PageViewController+UIPageViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageViewController+UIPageViewControllerDelegate.swift
3 | //
4 | //
5 | // Created by Benjamin Leonardo Sage on 11/19/20.
6 | //
7 |
8 | #if os(iOS)
9 |
10 | import UIKit
11 |
12 | extension PageViewController {
13 | class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
14 | var parent: PageViewController
15 | var willTransitionTo: (_ pageViewController: UIPageViewController,
16 | _ pendingViewControllers: [UIViewController]) -> Void
17 | var didFinishAnimating: (_ pageViewController: UIPageViewController,
18 | _ didFinishAnimating: Bool,
19 | _ previousViewControllers: [UIViewController],
20 | _ transitionCompleted: Bool) -> Void
21 |
22 | init(_ pageViewController: PageViewController,
23 | willTransitionTo: @escaping (_ pageViewController: UIPageViewController,
24 | _ pendingViewControllers: [UIViewController]) -> Void,
25 | didFinishAnimating: @escaping (_ pageViewController: UIPageViewController,
26 | _ didFinishAnimating: Bool,
27 | _ previousViewControllers: [UIViewController],
28 | _ transitionCompleted: Bool) -> Void) {
29 | self.parent = pageViewController
30 | self.willTransitionTo = willTransitionTo
31 | self.didFinishAnimating = didFinishAnimating
32 | }
33 |
34 | func pageViewController(
35 | _ pageViewController: UIPageViewController,
36 | viewControllerBefore viewController: UIViewController) -> UIViewController?
37 | {
38 | guard let index = parent.controllers.firstIndex(of: viewController) else {
39 | return nil
40 | }
41 | if index == 0 {
42 | if parent.wraps {
43 | return parent.controllers.last
44 | } else {
45 | return nil
46 | }
47 | }
48 | return parent.controllers[index - 1]
49 | }
50 |
51 | func pageViewController(
52 | _ pageViewController: UIPageViewController,
53 | viewControllerAfter viewController: UIViewController) -> UIViewController?
54 | {
55 | guard let index = parent.controllers.firstIndex(of: viewController) else {
56 | return nil
57 | }
58 | if index + 1 == parent.controllers.count {
59 | if parent.wraps {
60 | return parent.controllers.first
61 | } else {
62 | return nil
63 | }
64 | }
65 | return parent.controllers[index + 1]
66 | }
67 |
68 | func pageViewController(
69 | _ pageViewController: UIPageViewController,
70 | didFinishAnimating finished: Bool,
71 | previousViewControllers: [UIViewController],
72 | transitionCompleted completed: Bool)
73 | {
74 | if completed,
75 | let visibleViewController = pageViewController.viewControllers?.first,
76 | let index = parent.controllers.firstIndex(of: visibleViewController)
77 | {
78 | parent.currentPage = index
79 | }
80 | self.didFinishAnimating(pageViewController, finished, previousViewControllers, completed)
81 | }
82 |
83 | func pageViewController(
84 | _ pageViewController: UIPageViewController,
85 | willTransitionTo pendingViewControllers: [UIViewController])
86 | {
87 | self.willTransitionTo(pageViewController, pendingViewControllers)
88 | }
89 | }
90 | }
91 |
92 | #endif
93 |
--------------------------------------------------------------------------------
/Sources/iPages/PageViewController+UIScrollViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageViewControlle+UIScrollViewDelegate.swift
3 | //
4 | //
5 | // Created by Benjamin Sage on 10/23/20.
6 | //
7 |
8 | #if os(iOS)
9 | import UIKit
10 |
11 | extension PageViewController.Coordinator: UIScrollViewDelegate {
12 |
13 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
14 | if !parent.bounce {
15 | if parent.navigationOrientation == .horizontal {
16 | disableHorizontalBounce(scrollView)
17 | } else {
18 | disableVerticalBounce(scrollView)
19 | }
20 | }
21 | }
22 |
23 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
24 | scrollViewDidScroll(scrollView)
25 | }
26 |
27 | private func disableHorizontalBounce(_ scrollView: UIScrollView) {
28 | if parent.currentPage == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width ||
29 | parent.currentPage == self.parent.controllers.count - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width {
30 | scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
31 | }
32 | }
33 |
34 | private func disableVerticalBounce(_ scrollView: UIScrollView) {
35 | if parent.currentPage == 0 && scrollView.contentOffset.y < scrollView.bounds.size.height ||
36 | parent.currentPage == self.parent.controllers.count - 1 && scrollView.contentOffset.y > scrollView.bounds.size.height {
37 | scrollView.contentOffset = CGPoint(x: 0, y: scrollView.bounds.size.height)
38 | }
39 | }
40 | }
41 |
42 | #endif
43 |
--------------------------------------------------------------------------------
/Sources/iPages/PageViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageViewController.swift
3 | //
4 | //
5 | // Created by Benjamin Sage on 10/23/20.
6 | //
7 |
8 | import SwiftUI
9 | #if os(iOS)
10 | import UIKit
11 | #else
12 | import AppKit
13 | #endif
14 |
15 | struct PageViewController: ControllerRepresentable {
16 | var controllers: [ViewController]
17 | @Binding var currentPage: Int
18 |
19 | #if os(iOS)
20 | private var animated: Bool
21 | var wraps: Bool
22 | var navigationOrientation: UIPageViewController.NavigationOrientation
23 | var bounce: Bool
24 | private var interPageSpacing: CGFloat = 0
25 |
26 | // delegate
27 | var willTransitionTo: (_ pageViewController: UIPageViewController,
28 | _ pendingViewControllers: [UIViewController]) -> Void
29 | var didFinishAnimating: (_ pageViewController: UIPageViewController,
30 | _ didFinishAnimating: Bool,
31 | _ previousViewControllers: [UIViewController],
32 | _ transitionCompleted: Bool) -> Void
33 | #endif
34 |
35 | func makeCoordinator() -> Coordinator {
36 | Coordinator(self, willTransitionTo: willTransitionTo, didFinishAnimating: didFinishAnimating)
37 | }
38 |
39 | #if os(iOS)
40 | init(controllers: [ViewController],
41 | currentPage: Binding,
42 | wraps: Bool,
43 | navigationOrientation: UIPageViewController.NavigationOrientation,
44 | bounce: Bool,
45 | interPageSpacing: CGFloat,
46 | animated: Bool,
47 | willTransitionTo: @escaping (_ pageViewController: UIPageViewController,
48 | _ pendingViewControllers: [UIViewController]) -> Void = {_,_ in },
49 | didFinishAnimating: @escaping (_ pageViewController: UIPageViewController,
50 | _ didFinishAnimating: Bool,
51 | _ previousViewControllers: [UIViewController],
52 | _ transitionCompleted: Bool) -> Void = {_,_,_,_ in })
53 | {
54 | self.controllers = controllers
55 | self._currentPage = currentPage
56 | self.wraps = wraps
57 | self.navigationOrientation = navigationOrientation
58 | self.bounce = bounce
59 | self.interPageSpacing = interPageSpacing
60 | self.animated = animated
61 | self.willTransitionTo = willTransitionTo
62 | self.didFinishAnimating = didFinishAnimating
63 | }
64 |
65 | func makeUIViewController(context: Context) -> UIPageViewController {
66 | let options: [UIPageViewController.OptionsKey : Any] = [
67 | .interPageSpacing : interPageSpacing
68 | ]
69 | let pageViewController = UIPageViewController(
70 | transitionStyle: .scroll,
71 | navigationOrientation: navigationOrientation,
72 | options: options)
73 |
74 | pageViewController.dataSource = context.coordinator
75 | pageViewController.delegate = context.coordinator
76 |
77 | pageViewController.view.backgroundColor = .clear
78 |
79 | for view in pageViewController.view.subviews {
80 | if let scrollView = view as? UIScrollView {
81 | scrollView.delegate = context.coordinator
82 | break
83 | }
84 | }
85 |
86 | return pageViewController
87 | }
88 |
89 | func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
90 | let previousPage = context.coordinator.parent.currentPage
91 | context.coordinator.parent = self
92 |
93 | pageViewController.setViewControllers(
94 | [controllers[currentPage]],
95 | direction: currentPage > previousPage ? .forward : .reverse,
96 | animated: animated)
97 | }
98 | #else
99 | init(controllers: [ViewController],
100 | currentPage: Binding) {
101 | self.controllers = controllers
102 | self._currentPage = currentPage
103 | }
104 |
105 | func makeNSViewController(context: Context) -> NSPageController {
106 | let nsPageController = NSPageController()
107 | nsPageController.view = NSView()
108 |
109 | nsPageController.arrangedObjects = Array(0.. Void) -> iPages {
19 | var view = self
20 | view.willTransitionTo = delegate
21 | return view
22 | }
23 |
24 | func didFinishAnimationg(_ delegate: @escaping (_ pageViewController: UIPageViewController,
25 | _ didFinishAnimating: Bool,
26 | _ previousViewControllers: [UIViewController],
27 | _ transitionCompleted: Bool) -> Void ) -> iPages {
28 | var view = self
29 | view.didFinishAnimating = delegate
30 | return view
31 | }
32 |
33 | /// Modifies whether or not the page view should include the standard page control **dots**. (••••)
34 | /// - Parameter hideDots: Whether the page view should hide the page dots at the bottom 👇
35 | /// - Returns: A page view with the the desired presence or absence of dots
36 | func hideDots(_ hideDots: Bool) -> iPages {
37 | var view = self
38 | view.showsPageControl = !hideDots
39 | return view
40 | }
41 |
42 | #if os(iOS)
43 | /// Modifies whether the page dots are hidden when there is only one page. 1️⃣⤵️
44 | /// - Parameter hide: Whether the page dots are hidden when there is only one page
45 | /// - Returns: A page view with the desired dots hiding with one page settings
46 | func dotsHideForSinglePage(_ hide: Bool) -> iPages {
47 | var view = self
48 | view.pageControlHidesForSinglePage = hide
49 | return view
50 | }
51 | #endif
52 |
53 | /// Modifies **tint colors** 🟡🟢🔴🟣 to be used for the page dots.
54 | /// - Parameters:
55 | /// - currentPage: The tint color to be used for the current page dot ⬇️
56 | /// - otherPages: The Tint color to be used for dots which are not the current page⬅️➡️
57 | /// - Returns: A page view with the desired dot colors
58 | @available(iOS 14, *)
59 | func dotsTintColors(currentPage: Color, otherPages: Color) -> iPages {
60 | var view = self
61 | #if os(iOS)
62 | view.pageControlCurrentPageIndicatorTintColor = UIColor(currentPage)
63 | view.pageControlPageIndicatorTintColor = UIColor(otherPages)
64 | #else
65 | view.pageControlCurrentPageIndicatorTintColor = currentPage
66 | view.pageControlPageIndicatorTintColor = otherPages
67 | #endif
68 | return view
69 | }
70 |
71 | #if os(iOS)
72 | /// Modifies **tint colors** 🟡🟢🔴🟣 to be used for the page dots.
73 | /// - Parameters:
74 | /// - currentPage: The tint color to be used for the current page dot ⬇️
75 | /// - otherPages: The Tint color to be used for dots which are not the current page⬅️➡️
76 | /// - Returns: A page view with the desired dot colors
77 | @available(iOS, introduced: 13, obsoleted: 14)
78 | func dotsTintColors(currentPage: UIColor, otherPages: UIColor) -> iPages {
79 | var view = self
80 | view.pageControlCurrentPageIndicatorTintColor = currentPage
81 | view.pageControlPageIndicatorTintColor = otherPages
82 | return view
83 | }
84 |
85 | /// Modifies the **background style** ⚪️🔘 of the page dots.
86 | /// - Parameter style: The style of the background of the page dots
87 | /// - Returns: A page view with the desired background style of the dots
88 | @available(iOS 14, *)
89 | func dotsBackgroundStyle(_ style: UIPageControl.BackgroundStyle) -> iPages {
90 | var view = self
91 | view.pageControlBackgroundStyle = style
92 | return view
93 | }
94 |
95 | /// Modifies the continuous interaction settings of the dots. 🔄
96 | /// - Parameter allowContinuousInteraction: Whether the dots allow continuous interaction
97 | /// - Returns: A page view with the desired continuous interaction settings of the page dots
98 | @available(iOS 14, *)
99 | func dotsAllowContinuousInteraction(_ allowContinuousInteraction: Bool) -> iPages {
100 | var view = self
101 | view.pageControlAllowsContinuousInteraction = allowContinuousInteraction
102 | return view
103 | }
104 | #endif
105 |
106 | /// Modifies the **alignment of the page dots**. 👆 👇
107 | ///
108 | /// *Trailing* and *leading* alignments will cause the page dots to rotate vertical
109 | /// - Parameter alignment: Page dot alignment
110 | /// - Returns: A page view with the desired dots alignment
111 | func dotsAlignment(_ alignment: Alignment) -> iPages {
112 | var view = self
113 | view.pageControlAlignment = alignment
114 | return view
115 | }
116 |
117 | #if os(iOS)
118 | /// Modifies the navigation **orientation** of the page view. ↔️ ↕️
119 | ///
120 | /// By default, moves the page dots to the trailing edge
121 | /// - Parameter orientation: The navigation orientation, either horizontal or vertical.
122 | /// - Returns: A page view with the desired navigation orientation
123 | func navigationOrientation(_ orientation: UIPageViewController.NavigationOrientation) -> iPages {
124 | var view = self
125 | view.pageViewControllerNavigationOrientation = orientation
126 | if orientation == .vertical {
127 | view.pageControlAlignment = .trailing
128 | }
129 | return view
130 | }
131 |
132 | /// Disables the **bounce** settings of the page view.
133 | ///
134 | /// This is especially useful for scroll views.
135 | /// - Parameter disable: Whether the bounce settings should be disabled
136 | /// - Returns: A page view with the desired bounce settings
137 | func disableBounce(_ disable: Bool) -> iPages {
138 | var view = self
139 | view.pageViewControllerBounce = !disable
140 | return view
141 | }
142 |
143 | /// Modifies the spacing between the pages. ↔️
144 | /// - Parameter spacing: The spacing between pages, in Points. Defaults to 0.
145 | /// - Returns: A page view with modified inter-page spacing
146 | func interPageSpacing(_ spacing: CGFloat) -> iPages {
147 | var view = self
148 | view.pageViewControllerInterPageSpacing = spacing
149 | return view
150 | }
151 |
152 | /// Modifies whether or not the page view should **restart at the beginning** 🔁 when swiping past the end (and vise-versa).
153 | /// - Parameter wraps: Whether or not the page view wraps infinitely 🔄
154 | /// - Returns: A page view with the desired infinite wrap
155 | func wraps(_ wraps: Bool) -> iPages {
156 | var view = self
157 | view.pageViewControllerWraps = wraps
158 | return view
159 | }
160 |
161 | func animated(_ animated: Bool) -> iPages {
162 | var view = self
163 | view.pageViewAnimated = animated
164 | return view
165 | }
166 | #endif
167 | }
168 |
--------------------------------------------------------------------------------
/Sources/iPages/iPages.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | #if os(iOS)
3 | import UIKit
4 | #else
5 | import AppKit
6 | #endif
7 |
8 | /// A `View` wrapper for `UIPageViewController` which lets you write 📝 and use 🔨 a page view in SwiftUI.
9 | ///
10 | /// Binds to a zero-indexed 0️⃣1️⃣2️⃣ "current page" `Int`eger.
11 | public struct iPages: View {
12 | @State private var viewControllers: [ViewController]
13 |
14 | @State private var internalSelection: Int = 0
15 | @Binding private var externalSelection: Int
16 | private var selection: Binding {
17 | hasExternalSelection ? $externalSelection : $internalSelection
18 | }
19 | private var hasExternalSelection = false
20 |
21 | // delegate
22 | var willTransitionTo: ((_ pageViewController: UIPageViewController,
23 | _ pendingViewControllers: [UIViewController]) -> Void) = {_,_ in}
24 | var didFinishAnimating: ((_ pageViewController: UIPageViewController,
25 | _ didFinishAnimating: Bool,
26 | _ previousViewControllers: [UIViewController],
27 | _ transitionCompleted: Bool) -> Void) = {_,_,_,_ in }
28 |
29 | // Page view controller
30 | var pageViewControllerWraps: Bool = false
31 | var pageViewAnimated: Bool = true
32 | #if os(iOS)
33 | var pageViewControllerNavigationOrientation: UIPageViewController.NavigationOrientation = .horizontal
34 | var pageViewControllerBounce: Bool = true
35 | var pageViewControllerInterPageSpacing: CGFloat = 0
36 | #endif
37 | private var pageViewController: PageViewController {
38 | #if os(iOS)
39 | return .init(controllers: viewControllers,
40 | currentPage: selection,
41 | wraps: pageViewControllerWraps,
42 | navigationOrientation: pageViewControllerNavigationOrientation,
43 | bounce: pageViewControllerBounce,
44 | interPageSpacing: pageViewControllerInterPageSpacing,
45 | animated: pageViewAnimated,
46 | willTransitionTo: willTransitionTo,
47 | didFinishAnimating: didFinishAnimating)
48 | #else
49 | return .init(controllers: viewControllers,
50 | currentPage: selection)
51 | #endif
52 | }
53 |
54 | // Page control
55 | var pageControlAlignment: Alignment = .bottom
56 | var showsPageControl: Bool = true
57 | var pageControlHidesForSinglePage: Bool = false
58 | #if os(macOS)
59 | var pageControlCurrentPageIndicatorTintColor: Color?
60 | var pageControlPageIndicatorTintColor: Color?
61 | #endif
62 |
63 | #if os(iOS)
64 | var pageControlCurrentPageIndicatorTintColor: UIColor?
65 | var pageControlPageIndicatorTintColor: UIColor?
66 | private var _pageControlBackgroundStyle: Any? = nil
67 | @available(iOS 14, *)
68 | var pageControlBackgroundStyle: UIPageControl.BackgroundStyle {
69 | get {
70 | guard _pageControlBackgroundStyle != nil else {
71 | return .automatic
72 | }
73 | return _pageControlBackgroundStyle as! UIPageControl.BackgroundStyle
74 | }
75 | set(newStyle) {
76 | _pageControlBackgroundStyle = newStyle
77 | }
78 | }
79 | var pageControlAllowsContinuousInteraction: Bool = false
80 | private var pageControl: PageControl {
81 | if #available(iOS 14.0, *) {
82 | return .init(numberOfPages: viewControllers.count,
83 | currentPage: selection,
84 | hidesForSinglePage: pageControlHidesForSinglePage,
85 | pageIndicatorTintColor: pageControlPageIndicatorTintColor,
86 | currentPageIndicatorTintColor: pageControlCurrentPageIndicatorTintColor,
87 | backgroundStyle: pageControlBackgroundStyle,
88 | allowsContinuousInteraction: pageControlAllowsContinuousInteraction)
89 | } else {
90 | return .init(numberOfPages: viewControllers.count,
91 | currentPage: selection,
92 | hidesForSinglePage: pageControlHidesForSinglePage,
93 | pageIndicatorTintColor: pageControlPageIndicatorTintColor,
94 | currentPageIndicatorTintColor: pageControlCurrentPageIndicatorTintColor,
95 | allowsContinuousInteraction: pageControlAllowsContinuousInteraction)
96 | }
97 | }
98 | #else
99 | private var ipageControl: iPageControl {
100 | .init(numberOfPages: viewControllers.count,
101 | currentPage: selection,
102 | hidesForSinglePage: pageControlHidesForSinglePage,
103 | pageIndicatorTintColor: pageControlPageIndicatorTintColor,
104 | currentPageIndicatorTintColor: pageControlCurrentPageIndicatorTintColor)
105 | }
106 | #endif
107 |
108 | /// Initializes the page 📃📖 view. 👷♀️
109 | /// - Parameters:
110 | /// - selection: A binding to the page that the user is currently on ⌚️, zero indexed (meaning page 1 is 0, page 2 is 1, etc.)
111 | /// - content: The ordered view builder of `View`s to appear in the page view 📑
112 | public init(selection: Binding? = nil,
113 | @ViewBuilder content: () -> Content)
114 | {
115 | _viewControllers = State(initialValue: content().decompose().map { HostingController(rootView: $0) })
116 | if let selection = selection {
117 | _externalSelection = selection
118 | hasExternalSelection = true
119 | } else {
120 | _externalSelection = Binding(get: { 0 }, set: { _ in })
121 | }
122 | }
123 |
124 | @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
125 |
126 | public var body: some View {
127 | ZStack(alignment: pageControlAlignment) {
128 | pageViewController
129 | if showsPageControl {
130 | switch pageControlAlignment {
131 | case .leading, .trailing:
132 | VStack {
133 | if pageControlAlignment == .leading { Spacer() }
134 | #if os(iOS)
135 | pageControl
136 | #else
137 | ipageControl
138 | #endif
139 | if pageControlAlignment == .trailing { Spacer() }
140 | }
141 | .aspectRatio(1, contentMode: .fit)
142 | .rotationEffect(.degrees(layoutDirection ~= .leftToRight ? 90 : -90))
143 | default:
144 | #if os(iOS)
145 | pageControl
146 | .fixedSize()
147 | .padding(.vertical)
148 | #else
149 | ipageControl
150 | .fixedSize()
151 | .padding(.vertical)
152 | #endif
153 | }
154 | }
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import iPagesTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += iPagesTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/iPagesTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(iPagesTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Tests/iPagesTests/iPagesTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import iPages
3 |
4 | @available(iOS 13.0, *)
5 | final class iPagesTests: XCTestCase {
6 | func testExample() {
7 | // This is an example of a functional test case.
8 | // Use XCTAssert and related functions to verify your tests produce the correct
9 | // results.
10 | // XCTAssertEqual(iPages().text, "Hello, World!")
11 | }
12 |
13 | static var allTests = [
14 | ("testExample", testExample),
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/instructions/iPagesDemo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/instructions/iPagesDemo2.gif
--------------------------------------------------------------------------------
/instructions/iPagesDemoPrimary.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/instructions/iPagesDemoPrimary.gif
--------------------------------------------------------------------------------
/instructions/instructions-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/instructions/instructions-1.png
--------------------------------------------------------------------------------
/instructions/instructions-2-iPages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/instructions/instructions-2-iPages.png
--------------------------------------------------------------------------------
/instructions/instructions-3-iPages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blsage/iPages/e7969ede1cccfa907e11ab88af982227f5a23619/instructions/instructions-3-iPages.png
--------------------------------------------------------------------------------