├── .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 | ![](instructions/instructions-1.png) 8 | 9 | ## (2/3) Input https://github.com/benjaminsage/iPages.git & click Next. 10 | ![](instructions/instructions-2-iPages.png) 11 | 12 | ## (3/3) Select Version: Up to Next Major & click finish. 13 | ![](instructions/instructions-3-iPages.png) 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 | CI 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 --------------------------------------------------------------------------------