├── .codecov.yml ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Artwork ├── artwork.sketch └── logo.png ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dangerfile ├── Documentation ├── Pageboy 2.0 Migration Guide.md └── Pageboy 3 Migration Guide.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── Pageboy.podspec ├── Pageboy.xcconfig ├── Pageboy.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── README.md ├── Sources ├── .swiftlint.yml ├── Examples.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Example tvOS.xcscheme ├── Pageboy.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ ├── Pageboy iOS.xcscheme │ │ └── Pageboy tvOS.xcscheme ├── Pageboy │ ├── AutoScrolling │ │ ├── PageboyAutoScroller.swift │ │ └── PageboyViewController+AutoScrolling.swift │ ├── Info.plist │ ├── Model │ │ ├── NavigationDirection.swift │ │ └── Page.swift │ ├── Pageboy.h │ ├── PageboyViewController+Management.swift │ ├── PageboyViewController+ScrollCalculations.swift │ ├── PageboyViewController+ScrollDetection.swift │ ├── PageboyViewController+Updating.swift │ ├── PageboyViewController.swift │ ├── PrivacyInfo.xcprivacy │ ├── Protocols │ │ ├── PageboyViewControllerDataSource.swift │ │ └── PageboyViewControllerDelegate.swift │ ├── Transitioning │ │ ├── PageboyViewController+Transitioning.swift │ │ ├── TransitionOperation+Action.swift │ │ └── TransitionOperation.swift │ ├── UIViewController+Pageboy.swift │ └── Utilities │ │ ├── Extensions │ │ ├── DispatchQueue+main.swift │ │ ├── UIApplication+SafeShared.swift │ │ ├── UIPageViewController+ScrollView.swift │ │ ├── UIScrollView+Interaction.swift │ │ ├── UIView+Animation.swift │ │ ├── UIView+AutoLayout.swift │ │ └── UIView+Localization.swift │ │ ├── IndexedObjectMap.swift │ │ ├── PatchedPageViewController.swift │ │ └── WeakContainer.swift ├── PageboyTests │ ├── Info.plist │ ├── PageboyAutoScrollTests.swift │ ├── PageboyConfigurationTests.swift │ ├── PageboyDataSourceTests.swift │ ├── PageboyInsertionTests.swift │ ├── PageboyPropertyTests.swift │ ├── PageboyTests.swift │ ├── PageboyTransitionTests.swift │ └── TestComponents │ │ ├── TestPageChildViewController.swift │ │ ├── TestPageboyDataSource.swift │ │ ├── TestPageboyDelegate.swift │ │ └── TestPageboyViewController.swift ├── Shared │ ├── GradientBackgroundViewController.swift │ ├── PageboyStatusView.swift │ └── UIColor+Pageboy.swift ├── iOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon.png │ │ │ ├── Icon@2x.png │ │ │ ├── Icon@3x.png │ │ │ ├── IconPad.png │ │ │ ├── IconPad@2x.png │ │ │ └── IconPadPro.png │ │ ├── Contents.json │ │ ├── bg_logo_launch.imageset │ │ │ ├── Contents.json │ │ │ ├── bg_logo_launch.png │ │ │ ├── bg_logo_launch@2x.png │ │ │ └── bg_logo_launch@3x.png │ │ ├── ic_minus.imageset │ │ │ ├── Contents.json │ │ │ └── ic_minus.pdf │ │ ├── ic_plus.imageset │ │ │ ├── Contents.json │ │ │ └── ic_plus.pdf │ │ └── ic_welcome_icon.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_welcome_icon.png │ │ │ ├── ic_welcome_icon@2x.png │ │ │ └── ic_welcome_icon@3x.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ChildViewController.swift │ ├── Example iOS.entitlements │ ├── Extras │ │ ├── GradientBackgroundViewController+Appearance.swift │ │ ├── NavigationController.swift │ │ ├── Pageboy+NavigationNotifications.swift │ │ ├── PageboyTouchBar.swift │ │ ├── ToolbarDelegate.swift │ │ └── TransparentNavigationBar.swift │ ├── Info.plist │ ├── PageViewController.swift │ └── SceneDelegate.swift └── tvOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Background.png │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Foreground.png │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Middle.png │ │ │ │ └── Contents.json │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Background.png │ │ │ │ │ ├── Background@2x.png │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Foreground.png │ │ │ │ │ └── Foreground@2x.png │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── Middle.png │ │ │ │ └── Middle@2x.png │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Top Shelf Image.imageset │ │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ChildViewController.swift │ ├── Info.plist │ └── PageViewController.swift └── fastlane ├── Appfile └── Fastfile /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | 3 | ignore: 4 | - "Sources/PageboyTests/**/*" 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [msaps] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'gh-pages' 6 | 7 | jobs: 8 | Test: 9 | runs-on: macOS-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Prepare 13 | run: | 14 | bundle update --bundler 15 | bundle install 16 | - name: Run tests 17 | env: 18 | SLACK_URL: ${{ secrets.SLACK_URL }} 19 | run: bundle exec fastlane test 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | publish_release: 9 | runs-on: macOS-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Prepare 13 | run: | 14 | bundle update --bundler 15 | bundle install 16 | - name: Publish release 17 | env: 18 | SLACK_URL: ${{ secrets.SLACK_URL }} 19 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 20 | GITHUB_API_TOKEN: ${{ secrets.GH_UIAS_TOKEN }} 21 | run: bundle exec fastlane deploy 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/ 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | /fastlane/README.md 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Artwork/artwork.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Artwork/artwork.sketch -------------------------------------------------------------------------------- /Artwork/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Artwork/logo.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [uias@sapsford.tech](mailto:uias@sapsford.tech). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://www.contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | [version]: https://www.contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pageboy 2 | 3 | Thanks for your interest in contributing to Pageboy! Please have a read through this document for how you can help! 🎉 4 | 5 | You can help us reach that goal by contributing. Here are some ways you can contribute: 6 | 7 | - [Report any issues or bugs that you find](https://github.com/uias/Pageboy/issues/new) 8 | - [Open issues for any new features you'd like Pageboy to have](https://github.com/uias/Pageboy/issues/new) 9 | - [Implement other tasks selected for development](https://github.com/uias/Pageboy/issues?q=is%3Aissue+is%3Aopen+label%3A%22ready+for+development%22) 10 | - [Help answer questions asked by the community](https://github.com/uias/Pageboy/issues?q=is%3Aopen+is%3Aissue+label%3Aquestion) 11 | - [Spread the word about Pageboy](https://twitter.com/intent/tweet?text=Pageboy,%20UIPageViewController%20done%20properly:%20https://github.com/uias/Pageboy) 12 | 13 | ## Code of conduct 14 | 15 | All contributors are expected to follow our [Code of conduct](CONDUCT.md). 16 | Please read it before making any contributions. 17 | 18 | ## Setting up the project for development 19 | 20 | Nice and simple, clone the repo and then open `Pageboy.xcworkspace` in Xcode. 21 | 22 | The `Pageboy-Example` project is useful for manually testing `Pageboy`, featuring positional debugging labels and visual cues for ensuring everything is running smoothly. 😁 23 | 24 | ## Testing 25 | 26 | ### Running tests 27 | 28 | Tests should be added for all functionality, both when adding new behaviors to existing features, and implementing new ones. 29 | 30 | Pageboy uses `XCTest` to run its tests, which can either be run through Xcode or by running `$ swift test` in the repository. 31 | 32 | ## Architectural overview 33 | 34 | Here is a quick overview of the architecture of Pageboy, to help you orient yourself in the project. 35 | 36 | ### PageboyViewController 37 | 38 | This is the core class of the project, and is the main externally facing component. The class is split up into various extensions for feature segregation to make the project easier to navigate. 39 | 40 | ### Management 41 | 42 | All inner view controller management is contained within the [PageboyViewController+Management](https://github.com/uias/Pageboy/blob/main/Sources/Pageboy/PageboyViewController%2BManagement.swift) extension. 43 | 44 | This is also where the internal `UIPageViewController` instance is managed. Any additional functionality relevant to the `UIPageViewController` or management of child view controllers should be added to this extension. 45 | 46 | ### Scroll Detection 47 | 48 | The [PageboyViewController+ScrollDetection](https://github.com/uias/Pageboy/blob/main/Sources/Pageboy/PageboyViewController%2BScrollDetection.swift) extension handles responding to scroll updates in addition to all the functions for observing the internal scroll view. 49 | 50 | This extension also responds to the internal `UIPageViewControllerDelegate` and handles infinite scrolling behaviour etc. 51 | 52 | ### Transitioning 53 | 54 | The custom transitioning support available in `Pageboy` is provided by the [PageboyViewController+Transitioning](https://github.com/uias/Pageboy/blob/main/Sources/Pageboy/Transitioning/PageboyViewController%2BTransitioning.swift) extension. In conjunction with [TransitionOperation](https://github.com/uias/Pageboy/blob/main/Sources/Pageboy/Transitioning/TransitionOperation.swift) object, custom transitioning is made available through the use of `CATransition` in place of the built in `UIPageViewController` animations. 55 | 56 | Any updates or tweaks to animated transitioning should be made here. 57 | 58 | ## Questions or discussions 59 | 60 | If you have a question about the inner workings of Pageboy, or if you want to discuss a new feature - feel free to [open an issue](https://github.com/uias/Pageboy/issues/new). 61 | 62 | Happy contributing! 👨🏻‍💻 63 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | 2 | has_source_changes = !git.modified_files.grep(/Sources.*\.swift/).empty? || !git.added_files.grep(/Sources.*\.swift/).empty? 3 | has_tests_changes = !git.modified_files.grep(/Sources\/PageboyTests.*\.swift/).empty? || !git.added_files.grep(/Sources\/PageboyTests.*\.swift/).empty? 4 | 5 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet 6 | warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" 7 | 8 | # Warn when there is a big PR 9 | warn("This PR might be a little too big, consider breaking it up.") if git.lines_of_code > 500 10 | 11 | # Require PR description 12 | warn "Please provide a summary in the PR description, it makes it easier to understand!" if github.pr_body.length < 5 13 | 14 | # Check for source changes and prompt for test updates if non added. 15 | if (has_source_changes && ! has_tests_changes) 16 | warn("Looks like you changed some source files, should there have been some tests added?") 17 | end 18 | 19 | swiftlint.config_file = 'Sources/.swiftlint.yml' 20 | swiftlint.lint_files -------------------------------------------------------------------------------- /Documentation/Pageboy 2.0 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # Pageboy 2.0 Migration Guide 2 | 3 | Pageboy 2.0 is the latest major release of Pageboy; a simple, highly informative page view controller for iOS. Pageboy 2.0 introduces several API-breaking changes that should be made aware of. 4 | 5 | This guide aims to provide an easy transition from existing implementations of Pageboy 1.x to the newest API's available in Pageboy 2.0. 6 | 7 | ## Requirements 8 | 9 | - iOS 8.0+ 10 | - Xcode 9.0+ 11 | - Swift 4.0+ 12 | 13 | For anyone wanting to use Pageboy with a Swift 3.x project, please use the latest 1.x release. 14 | 15 | ## What's new 16 | 17 | - **Full Swift 4 Compatibility** 18 | - **iOS 11 Compatibility** 19 | - **Redesigned Data Source:** `PageboyViewControllerDataSource` has been completely redesigned to promote easier reuse and configuration of view controllers. 20 | - **Refactored Delegate:** `PageboyViewControllerDelegate` functions have been refactored to support the latest design guidelines and the redesigned data source. 21 | 22 | ## API Changes 23 | 24 | There are significant changes to the data source and delegates associated with `PageboyViewController` in Pageboy 2.0. 25 | 26 | ### Data Source Changes 27 | `PageboyViewControllerDataSource` has been significantly changed to provide better support for reuse and performance when using large amounts of pages. The basic gist is that rather than returning a static array of view controllers, the data source can now be provided with dynamic view controllers per page index. 28 | 29 | ```swift 30 | // Pageboy 1.x 31 | func viewControllers(forPageboyViewController pageboyViewController: PageboyViewController) -> [UIViewController]? { 32 | return [viewController1, viewController2] 33 | } 34 | 35 | // Pageboy 2.x 36 | func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> PageboyViewController.PageIndex { 37 | return 2 38 | } 39 | 40 | func viewController(for pageboyViewController: PageboyViewController, 41 | at index: PageboyViewController.PageIndex) -> UIViewController? { 42 | return self.viewControllers[index] 43 | } 44 | ``` 45 | 46 | The syntax for other data source methods has also been updated to conform to the latest Swift standards. 47 | 48 | ```swift 49 | // Pageboy 1.x 50 | func defaultPageIndex(forPageboyViewController pageboyViewController: PageboyViewController) -> PageboyViewController.PageIndex? { 51 | return nil 52 | } 53 | 54 | // Pageboy 2.x 55 | func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { 56 | return nil 57 | } 58 | ``` 59 | 60 | ### Delegate Changes 61 | `PageboyViewControllerDelegate` has also been significantly modified to support the new data source functions and an updated syntax style. 62 | 63 | #### willScrollToPage 64 | ```swift 65 | // Pageboy 1.x 66 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 67 | willScrollToPageAtIndex index: Int, 68 | direction: PageboyViewController.NavigationDirection, 69 | animated: Bool) 70 | 71 | // Pageboy 2.x 72 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 73 | willScrollToPageAt index: PageboyViewController.PageIndex, 74 | direction: PageboyViewController.NavigationDirection, 75 | animated: Bool) 76 | ``` 77 | 78 | #### didScrollToPosition 79 | ```swift 80 | // Pageboy 1.x 81 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 82 | didScrollToPosition position: CGPoint, 83 | direction: PageboyViewController.NavigationDirection) 84 | 85 | // Pageboy 2.x 86 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 87 | didScrollTo position: CGPoint, 88 | direction: PageboyViewController.NavigationDirection, 89 | animated: Bool) 90 | ``` 91 | 92 | #### didScrollToPage 93 | ```swift 94 | // Pageboy 1.x 95 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 96 | didScrollToPageAtIndex index: Int, 97 | direction: PageboyViewController.NavigationDirection, 98 | animated: Bool) 99 | 100 | // Pageboy 2.x 101 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 102 | didScrollToPageAt index: PageboyViewController.PageIndex, 103 | direction: PageboyViewController.NavigationDirection, 104 | animated: Bool) 105 | ``` 106 | 107 | #### didReload 108 | ```swift 109 | // Pageboy 1.x 110 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 111 | didReload viewControllers: [UIViewController], 112 | currentIndex: PageboyViewController.PageIndex) 113 | 114 | // Pageboy 2.x 115 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 116 | didReloadWith currentViewController: UIViewController, 117 | currentPageIndex: PageboyViewController.PageIndex) 118 | ``` 119 | 120 | ### Type Changes 121 | 122 | - `PageboyViewController.PageIndex` is now `PageboyViewController.Page`. 123 | - `PageboyViewController.PageIndex` refers to an `Int` `typealias` for describing raw page index values. 124 | 125 | ### Property Updates 126 | - `pageCount` has been added to `PageboyViewController`. 127 | - `viewControllers` has been removed from `PageboyViewController`. -------------------------------------------------------------------------------- /Documentation/Pageboy 3 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # Pageboy 3 Migration Guide 2 | 3 | Pageboy 3 is the latest major release of Pageboy; a simple, highly informative page view controller for iOS. Pageboy 3 introduces several API-breaking changes that should be made aware of. 4 | 5 | This guide aims to provide an easy transition from existing implementations of Pageboy 2.x to the newest API's available in Pageboy 3. 6 | 7 | ## Requirements 8 | 9 | - iOS 9.0+ 10 | - Xcode 10.0+ 11 | - Swift 4.0+ 12 | 13 | ## What's new 14 | 15 | - View Controllers can now be inserted and removed dynamically. 16 | - Fixed numerous performance and memory issues. 17 | - Improved Swift 4 & 4.2 compatibility. 18 | 19 | ## API Changes 20 | 21 | ### PageboyViewControllerDelegate 22 | - Default implementations of `PageboyViewControllerDelegate` have been removed - effectively requiring all functions to be implemented. 23 | 24 | ### Properties 25 | - `showsPageControl` has now been removed completely due to [#128](https://github.com/uias/Pageboy/issues/128). 26 | 27 | ### Functions 28 | - `reloadPages()` is now `reloadData()`. 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org' 3 | 4 | gem 'fastlane' 5 | gem 'cocoapods' 6 | gem 'danger' 7 | gem 'danger-swiftlint' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.2.1) 9 | base64 10 | bigdecimal 11 | concurrent-ruby (~> 1.0, >= 1.3.1) 12 | connection_pool (>= 2.2.5) 13 | drb 14 | i18n (>= 1.6, < 2) 15 | logger (>= 1.4.2) 16 | minitest (>= 5.1) 17 | securerandom (>= 0.3) 18 | tzinfo (~> 2.0, >= 2.0.5) 19 | addressable (2.8.7) 20 | public_suffix (>= 2.0.2, < 7.0) 21 | algoliasearch (1.27.5) 22 | httpclient (~> 2.8, >= 2.8.3) 23 | json (>= 1.5.1) 24 | artifactory (3.0.17) 25 | atomos (0.1.3) 26 | aws-eventstream (1.3.0) 27 | aws-partitions (1.972.0) 28 | aws-sdk-core (3.203.0) 29 | aws-eventstream (~> 1, >= 1.3.0) 30 | aws-partitions (~> 1, >= 1.651.0) 31 | aws-sigv4 (~> 1.9) 32 | jmespath (~> 1, >= 1.6.1) 33 | aws-sdk-kms (1.89.0) 34 | aws-sdk-core (~> 3, >= 3.203.0) 35 | aws-sigv4 (~> 1.5) 36 | aws-sdk-s3 (1.160.0) 37 | aws-sdk-core (~> 3, >= 3.203.0) 38 | aws-sdk-kms (~> 1) 39 | aws-sigv4 (~> 1.5) 40 | aws-sigv4 (1.9.1) 41 | aws-eventstream (~> 1, >= 1.0.2) 42 | babosa (1.0.4) 43 | base64 (0.2.0) 44 | bigdecimal (3.1.8) 45 | claide (1.1.0) 46 | claide-plugins (0.9.2) 47 | cork 48 | nap 49 | open4 (~> 1.3) 50 | cocoapods (1.15.2) 51 | addressable (~> 2.8) 52 | claide (>= 1.0.2, < 2.0) 53 | cocoapods-core (= 1.15.2) 54 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 55 | cocoapods-downloader (>= 2.1, < 3.0) 56 | cocoapods-plugins (>= 1.0.0, < 2.0) 57 | cocoapods-search (>= 1.0.0, < 2.0) 58 | cocoapods-trunk (>= 1.6.0, < 2.0) 59 | cocoapods-try (>= 1.1.0, < 2.0) 60 | colored2 (~> 3.1) 61 | escape (~> 0.0.4) 62 | fourflusher (>= 2.3.0, < 3.0) 63 | gh_inspector (~> 1.0) 64 | molinillo (~> 0.8.0) 65 | nap (~> 1.0) 66 | ruby-macho (>= 2.3.0, < 3.0) 67 | xcodeproj (>= 1.23.0, < 2.0) 68 | cocoapods-core (1.15.2) 69 | activesupport (>= 5.0, < 8) 70 | addressable (~> 2.8) 71 | algoliasearch (~> 1.0) 72 | concurrent-ruby (~> 1.1) 73 | fuzzy_match (~> 2.0.4) 74 | nap (~> 1.0) 75 | netrc (~> 0.11) 76 | public_suffix (~> 4.0) 77 | typhoeus (~> 1.0) 78 | cocoapods-deintegrate (1.0.5) 79 | cocoapods-downloader (2.1) 80 | cocoapods-plugins (1.0.0) 81 | nap 82 | cocoapods-search (1.0.1) 83 | cocoapods-trunk (1.6.0) 84 | nap (>= 0.8, < 2.0) 85 | netrc (~> 0.11) 86 | cocoapods-try (1.2.0) 87 | colored (1.2) 88 | colored2 (3.1.2) 89 | commander (4.6.0) 90 | highline (~> 2.0.0) 91 | concurrent-ruby (1.3.4) 92 | connection_pool (2.4.1) 93 | cork (0.3.0) 94 | colored2 (~> 3.1) 95 | danger (9.5.0) 96 | claide (~> 1.0) 97 | claide-plugins (>= 0.9.2) 98 | colored2 (~> 3.1) 99 | cork (~> 0.1) 100 | faraday (>= 0.9.0, < 3.0) 101 | faraday-http-cache (~> 2.0) 102 | git (~> 1.13) 103 | kramdown (~> 2.3) 104 | kramdown-parser-gfm (~> 1.0) 105 | octokit (>= 4.0) 106 | terminal-table (>= 1, < 4) 107 | danger-swiftlint (0.36.1) 108 | danger 109 | rake (> 10) 110 | thor (~> 1.0.0) 111 | declarative (0.0.20) 112 | digest-crc (0.6.5) 113 | rake (>= 12.0.0, < 14.0.0) 114 | domain_name (0.6.20240107) 115 | dotenv (2.8.1) 116 | drb (2.2.1) 117 | emoji_regex (3.2.3) 118 | escape (0.0.4) 119 | ethon (0.16.0) 120 | ffi (>= 1.15.0) 121 | excon (0.111.0) 122 | faraday (1.10.3) 123 | faraday-em_http (~> 1.0) 124 | faraday-em_synchrony (~> 1.0) 125 | faraday-excon (~> 1.1) 126 | faraday-httpclient (~> 1.0) 127 | faraday-multipart (~> 1.0) 128 | faraday-net_http (~> 1.0) 129 | faraday-net_http_persistent (~> 1.0) 130 | faraday-patron (~> 1.0) 131 | faraday-rack (~> 1.0) 132 | faraday-retry (~> 1.0) 133 | ruby2_keywords (>= 0.0.4) 134 | faraday-cookie_jar (0.0.7) 135 | faraday (>= 0.8.0) 136 | http-cookie (~> 1.0.0) 137 | faraday-em_http (1.0.0) 138 | faraday-em_synchrony (1.0.0) 139 | faraday-excon (1.1.0) 140 | faraday-http-cache (2.5.1) 141 | faraday (>= 0.8) 142 | faraday-httpclient (1.0.1) 143 | faraday-multipart (1.0.4) 144 | multipart-post (~> 2) 145 | faraday-net_http (1.0.2) 146 | faraday-net_http_persistent (1.2.0) 147 | faraday-patron (1.0.0) 148 | faraday-rack (1.0.0) 149 | faraday-retry (1.0.3) 150 | faraday_middleware (1.2.0) 151 | faraday (~> 1.0) 152 | fastimage (2.3.1) 153 | fastlane (2.222.0) 154 | CFPropertyList (>= 2.3, < 4.0.0) 155 | addressable (>= 2.8, < 3.0.0) 156 | artifactory (~> 3.0) 157 | aws-sdk-s3 (~> 1.0) 158 | babosa (>= 1.0.3, < 2.0.0) 159 | bundler (>= 1.12.0, < 3.0.0) 160 | colored (~> 1.2) 161 | commander (~> 4.6) 162 | dotenv (>= 2.1.1, < 3.0.0) 163 | emoji_regex (>= 0.1, < 4.0) 164 | excon (>= 0.71.0, < 1.0.0) 165 | faraday (~> 1.0) 166 | faraday-cookie_jar (~> 0.0.6) 167 | faraday_middleware (~> 1.0) 168 | fastimage (>= 2.1.0, < 3.0.0) 169 | gh_inspector (>= 1.1.2, < 2.0.0) 170 | google-apis-androidpublisher_v3 (~> 0.3) 171 | google-apis-playcustomapp_v1 (~> 0.1) 172 | google-cloud-env (>= 1.6.0, < 2.0.0) 173 | google-cloud-storage (~> 1.31) 174 | highline (~> 2.0) 175 | http-cookie (~> 1.0.5) 176 | json (< 3.0.0) 177 | jwt (>= 2.1.0, < 3) 178 | mini_magick (>= 4.9.4, < 5.0.0) 179 | multipart-post (>= 2.0.0, < 3.0.0) 180 | naturally (~> 2.2) 181 | optparse (>= 0.1.1, < 1.0.0) 182 | plist (>= 3.1.0, < 4.0.0) 183 | rubyzip (>= 2.0.0, < 3.0.0) 184 | security (= 0.1.5) 185 | simctl (~> 1.6.3) 186 | terminal-notifier (>= 2.0.0, < 3.0.0) 187 | terminal-table (~> 3) 188 | tty-screen (>= 0.6.3, < 1.0.0) 189 | tty-spinner (>= 0.8.0, < 1.0.0) 190 | word_wrap (~> 1.0.0) 191 | xcodeproj (>= 1.13.0, < 2.0.0) 192 | xcpretty (~> 0.3.0) 193 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) 194 | ffi (1.17.0-arm64-darwin) 195 | ffi (1.17.0-x86_64-darwin) 196 | fourflusher (2.3.1) 197 | fuzzy_match (2.0.4) 198 | gh_inspector (1.1.3) 199 | git (1.19.1) 200 | addressable (~> 2.8) 201 | rchardet (~> 1.8) 202 | google-apis-androidpublisher_v3 (0.54.0) 203 | google-apis-core (>= 0.11.0, < 2.a) 204 | google-apis-core (0.11.3) 205 | addressable (~> 2.5, >= 2.5.1) 206 | googleauth (>= 0.16.2, < 2.a) 207 | httpclient (>= 2.8.1, < 3.a) 208 | mini_mime (~> 1.0) 209 | representable (~> 3.0) 210 | retriable (>= 2.0, < 4.a) 211 | rexml 212 | google-apis-iamcredentials_v1 (0.17.0) 213 | google-apis-core (>= 0.11.0, < 2.a) 214 | google-apis-playcustomapp_v1 (0.13.0) 215 | google-apis-core (>= 0.11.0, < 2.a) 216 | google-apis-storage_v1 (0.31.0) 217 | google-apis-core (>= 0.11.0, < 2.a) 218 | google-cloud-core (1.7.1) 219 | google-cloud-env (>= 1.0, < 3.a) 220 | google-cloud-errors (~> 1.0) 221 | google-cloud-env (1.6.0) 222 | faraday (>= 0.17.3, < 3.0) 223 | google-cloud-errors (1.4.0) 224 | google-cloud-storage (1.47.0) 225 | addressable (~> 2.8) 226 | digest-crc (~> 0.4) 227 | google-apis-iamcredentials_v1 (~> 0.1) 228 | google-apis-storage_v1 (~> 0.31.0) 229 | google-cloud-core (~> 1.6) 230 | googleauth (>= 0.16.2, < 2.a) 231 | mini_mime (~> 1.0) 232 | googleauth (1.8.1) 233 | faraday (>= 0.17.3, < 3.a) 234 | jwt (>= 1.4, < 3.0) 235 | multi_json (~> 1.11) 236 | os (>= 0.9, < 2.0) 237 | signet (>= 0.16, < 2.a) 238 | highline (2.0.3) 239 | http-cookie (1.0.7) 240 | domain_name (~> 0.5) 241 | httpclient (2.8.3) 242 | i18n (1.14.5) 243 | concurrent-ruby (~> 1.0) 244 | jmespath (1.6.2) 245 | json (2.7.2) 246 | jwt (2.8.2) 247 | base64 248 | kramdown (2.4.0) 249 | rexml 250 | kramdown-parser-gfm (1.1.0) 251 | kramdown (~> 2.0) 252 | logger (1.6.1) 253 | mini_magick (4.13.2) 254 | mini_mime (1.1.5) 255 | minitest (5.25.1) 256 | molinillo (0.8.0) 257 | multi_json (1.15.0) 258 | multipart-post (2.4.1) 259 | nanaimo (0.3.0) 260 | nap (1.1.0) 261 | naturally (2.2.1) 262 | netrc (0.11.0) 263 | nkf (0.2.0) 264 | octokit (9.1.0) 265 | faraday (>= 1, < 3) 266 | sawyer (~> 0.9) 267 | open4 (1.3.4) 268 | optparse (0.5.0) 269 | os (1.1.4) 270 | plist (3.7.1) 271 | public_suffix (4.0.7) 272 | rake (13.2.1) 273 | rchardet (1.8.0) 274 | representable (3.2.0) 275 | declarative (< 0.1.0) 276 | trailblazer-option (>= 0.1.1, < 0.2.0) 277 | uber (< 0.2.0) 278 | retriable (3.1.2) 279 | rexml (3.3.7) 280 | rouge (2.0.7) 281 | ruby-macho (2.5.1) 282 | ruby2_keywords (0.0.5) 283 | rubyzip (2.3.2) 284 | sawyer (0.9.2) 285 | addressable (>= 2.3.5) 286 | faraday (>= 0.17.3, < 3) 287 | securerandom (0.3.1) 288 | security (0.1.5) 289 | signet (0.19.0) 290 | addressable (~> 2.8) 291 | faraday (>= 0.17.5, < 3.a) 292 | jwt (>= 1.5, < 3.0) 293 | multi_json (~> 1.10) 294 | simctl (1.6.10) 295 | CFPropertyList 296 | naturally 297 | terminal-notifier (2.0.0) 298 | terminal-table (3.0.2) 299 | unicode-display_width (>= 1.1.1, < 3) 300 | thor (1.0.1) 301 | trailblazer-option (0.1.2) 302 | tty-cursor (0.7.1) 303 | tty-screen (0.8.2) 304 | tty-spinner (0.9.3) 305 | tty-cursor (~> 0.7) 306 | typhoeus (1.4.1) 307 | ethon (>= 0.9.0) 308 | tzinfo (2.0.6) 309 | concurrent-ruby (~> 1.0) 310 | uber (0.1.0) 311 | unicode-display_width (2.5.0) 312 | word_wrap (1.0.0) 313 | xcodeproj (1.25.0) 314 | CFPropertyList (>= 2.3.3, < 4.0) 315 | atomos (~> 0.1.3) 316 | claide (>= 1.0.2, < 2.0) 317 | colored2 (~> 3.1) 318 | nanaimo (~> 0.3.0) 319 | rexml (>= 3.3.2, < 4.0) 320 | xcpretty (0.3.0) 321 | rouge (~> 2.0.7) 322 | xcpretty-travis-formatter (1.0.1) 323 | xcpretty (~> 0.2, >= 0.0.7) 324 | 325 | PLATFORMS 326 | arm64-darwin-22 327 | x86_64-darwin-21 328 | 329 | DEPENDENCIES 330 | cocoapods 331 | danger 332 | danger-swiftlint 333 | fastlane 334 | 335 | BUNDLED WITH 336 | 2.4.19 337 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 UI At Six 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.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Pageboy", 8 | platforms: [ 9 | .iOS(.v12), 10 | .tvOS(.v12) 11 | ], 12 | products: [ 13 | .library( 14 | name: "Pageboy", 15 | targets: ["Pageboy"]) 16 | ], 17 | targets: [ 18 | .target( 19 | name: "Pageboy", 20 | path: "Sources/Pageboy", 21 | exclude: ["Pageboy.h", "PrivacyInfo.xcprivacy"], 22 | resources: [.process("PrivacyInfo.xcprivacy")] 23 | ), 24 | .testTarget( 25 | name: "PageboyTests", 26 | dependencies: ["Pageboy"] 27 | ) 28 | ], 29 | swiftLanguageVersions: [.v5] 30 | ) 31 | -------------------------------------------------------------------------------- /Pageboy.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "Pageboy" 4 | 5 | s.ios.deployment_target = '12.0' 6 | s.tvos.deployment_target = '12.0' 7 | 8 | s.swift_versions = ['5.0'] 9 | 10 | s.requires_arc = true 11 | 12 | s.version = "4.2.0" 13 | s.summary = "A simple, highly informative page view controller." 14 | s.description = <<-DESC 15 | A page view controller that provides simplified data source management, enhanced delegation and other useful features. 16 | DESC 17 | 18 | s.homepage = "https://github.com/uias/Pageboy" 19 | s.license = "MIT" 20 | s.author = { "Merrick Sapsford" => "merrick@sapsford.tech" } 21 | s.social_media_url = "https://twitter.com/MerrickSapsford" 22 | 23 | s.source = { :git => "https://github.com/uias/Pageboy.git", :tag => s.version.to_s } 24 | s.source_files = "Sources/Pageboy/**/*.{h,m,swift}" 25 | 26 | s.resource_bundles = {'Pageboy' => ['Sources/Pageboy/PrivacyInfo.xcprivacy']} 27 | 28 | end 29 | -------------------------------------------------------------------------------- /Pageboy.xcconfig: -------------------------------------------------------------------------------- 1 | PB_VERSION=4.2.0 2 | 3 | PB_IOS_DEPLOYMENT_TARGET=12.0 4 | PB_TVOS_DEPLOYMENT_TARGET=12.0 5 | -------------------------------------------------------------------------------- /Pageboy.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Pageboy.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Pageboy 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 | **TL;DR** *UIPageViewController done properly.* 19 | 20 | ## ⭐️ Features 21 | - [x] Simplified data source management & enhanced delegation. 22 | - [x] Dynamically insert & remove pages. 23 | - [x] Infinite scrolling support. 24 | - [x] Automatic timer-based page transitioning. 25 | - [x] Support for custom animated page transitions. 26 | 27 | ## 📋 Requirements 28 | Pageboy requires iOS 12 / tvOS 12; and is compatible with Swift 5. 29 | 30 | ## 📲 Installation 31 | 32 | ### Swift Package Manager 33 | Pageboy is compatible with [Swift Package Manager](https://swift.org/package-manager) and can be integrated via Xcode. 34 | 35 | ### CocoaPods 36 | Pageboy is also available through [CocoaPods](https://cocoapods.org): 37 | ```ruby 38 | pod 'Pageboy', '~> 4.2' 39 | ``` 40 | 41 | ### Carthage 42 | Pageboy is also available through [Carthage](https://github.com/Carthage/Carthage): 43 | ```ogdl 44 | github "uias/Pageboy" ~> 4.2 45 | ``` 46 | 47 | ## 🚀 Usage 48 | - [The Basics](#the-basics) 49 | - [PageboyViewControllerDelegate](#pageboyViewControllerDelegate) 50 | - [Navigation](#navigation) 51 | - [Insertion & Deletion](#insertion-&-deletion) 52 | 53 | ### The Basics 54 | 55 | 1) Create an instance of a `PageboyViewController` and provide it with a `PageboyViewControllerDataSource`. 56 | 57 | ```swift 58 | class PageViewController: PageboyViewController, PageboyViewControllerDataSource { 59 | 60 | override func viewDidLoad() { 61 | super.viewDidLoad() 62 | 63 | self.dataSource = self 64 | } 65 | } 66 | ``` 67 | 68 | 2) Implement the `PageboyViewControllerDataSource` functions. 69 | 70 | ```swift 71 | func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { 72 | return viewControllers.count 73 | } 74 | 75 | func viewController(for pageboyViewController: PageboyViewController, 76 | at index: PageboyViewController.PageIndex) -> UIViewController? { 77 | return viewControllers[index] 78 | } 79 | 80 | func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { 81 | return nil 82 | } 83 | ``` 84 | 85 | ### PageboyViewControllerDelegate 86 | 87 | The delegate functions provided by a `PageboyViewController` are much more reliable and useful than what a raw `UIPageViewController` provides. You can use them to find out exactly where the current page is, and when it's moved, where it's headed. 88 | 89 | #### willScrollToPageAtIndex 90 | About to embark on a transition to a new page. 91 | 92 | ```swift 93 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 94 | willScrollToPageAt index: Int, 95 | direction: PageboyViewController.NavigationDirection, 96 | animated: Bool) 97 | ``` 98 | 99 | #### didScrollToPosition 100 | Scrolled to a relative position along the way transitioning to a new page. 101 | 102 | ```swift 103 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 104 | didScrollTo position: CGPoint, 105 | direction: PageboyViewController.NavigationDirection, 106 | animated: Bool) 107 | ``` 108 | 109 | #### didScrollToPage 110 | Successfully completed a scroll transition to a page. 111 | 112 | ```swift 113 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 114 | didScrollToPageAt index: Int, 115 | direction: PageboyViewController.NavigationDirection, 116 | animated: Bool) 117 | ``` 118 | 119 | #### didReload 120 | Child view controllers have been reloaded. 121 | 122 | ```swift 123 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 124 | didReloadWith currentViewController: UIViewController, 125 | currentPageIndex: PageIndex) 126 | ``` 127 | 128 | ### Navigation 129 | You can navigate programmatically through a `PageboyViewController` using `scrollToPage()`: 130 | ```swift 131 | pageViewController.scrollToPage(.next, animated: true) 132 | ``` 133 | 134 | - Infinite scrolling can be enabled with `.isInfiniteScrollEnabled`. 135 | - Interactive scrolling can also be controlled with `.isScrollEnabled`. 136 | 137 | ### Insertion & Deletion 138 | Pageboy provides the ability to insert and delete pages dynamically in the `PageboyViewController`. 139 | 140 | ```swift 141 | func insertPage(at index: PageIndex, then updateBehavior: PageUpdateBehavior) 142 | func deletePage(at index: PageIndex, then updateBehavior: PageUpdateBehavior) 143 | ``` 144 | 145 | *This behaves similarly to the insertion of rows in `UITableView`, in the fact that you have to update the data source prior to calling any of the update functions.* 146 | 147 | **Example:** 148 | 149 | ```swift 150 | let index = 2 151 | viewControllers.insert(UIViewController(), at: index) 152 | pageViewController.insertPage(at: index) 153 | ``` 154 | 155 | *The default behavior after inserting or deleting a page is to scroll to the update location, this however can be configured by passing a `PageUpdateBehavior` value other than `.scrollToUpdate`.* 156 | 157 | ## ⚡️ Other Extras 158 | 159 | - `reloadData()` - Reload the view controllers in the page view controller. (Reloads the data source). 160 | - `.navigationOrientation` - Whether to orientate the pages horizontally or vertically. 161 | - `.currentViewController` - The currently visible view controller if it exists. 162 | - `.currentPosition` - The exact current relative position of the page view controller. 163 | - `.currentIndex` - The index of the currently visible page. 164 | - `.parentPageboy` - Access the immediate parent `PageboyViewController` from any child view controller. 165 | 166 | ### Animated Transitions 167 | Pageboy also provides custom transition support for **animated transitions**. This can be customized via the `.transition` property on `PageboyViewController`. 168 | 169 | ```swift 170 | pageboyViewController.transition = Transition(style: .push, duration: 1.0) 171 | ``` 172 | 173 | *Note: By default this is set to `nil`, which uses the standard animation provided by `UIPageViewController`.* 174 | 175 | ### Auto Scrolling 176 | `PageboyAutoScroller` is available to set up timer based automatic scrolling of the `PageboyViewController`: 177 | 178 | ```swift 179 | pageboyViewController.autoScroller.enable() 180 | ``` 181 | 182 | Support for custom intermission duration and other scroll behaviors is also available. 183 | 184 | ## 👨🏻‍💻 About 185 | - Created by [Merrick Sapsford](https://github.com/msaps) ([@MerrickSapsford](https://twitter.com/MerrickSapsford)) 186 | - Contributed to by a growing [list of others](https://github.com/uias/Pageboy/graphs/contributors). 187 | 188 | ## ❤️ Contributing 189 | Bug reports and pull requests are welcome on GitHub at [https://github.com/uias/Pageboy](https://github.com/uias/Pageboy). 190 | 191 | ## 👮🏻‍♂️ License 192 | The library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 193 | -------------------------------------------------------------------------------- /Sources/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - vertical_whitespace 4 | - line_length 5 | - opening_brace 6 | - large_tuple 7 | - nesting 8 | - function_body_length 9 | 10 | opt_in_rules: 11 | - closure_spacing 12 | - conditional_returns_on_newline 13 | #- empty_count 14 | - explicit_init 15 | - force_unwrapping 16 | #- missing_docs 17 | - overridden_super_call 18 | - private_outlet 19 | - switch_case_on_newline 20 | 21 | excluded: 22 | - Pods 23 | - PageboyTests 24 | -------------------------------------------------------------------------------- /Sources/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Examples.xcodeproj/xcshareddata/xcschemes/Example tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Sources/Pageboy.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Pageboy.xcodeproj/xcshareddata/xcschemes/Pageboy iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Sources/Pageboy.xcodeproj/xcshareddata/xcschemes/Pageboy tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Sources/Pageboy/AutoScrolling/PageboyAutoScroller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyAutoScroller.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 08/03/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Internal protocol for handling auto scroller events. 12 | internal protocol PageboyAutoScrollerHandler: AnyObject { 13 | 14 | /// Auto scroller requires a scroll. 15 | /// 16 | /// - Parameter autoScroller: The auto scroller. 17 | /// - Parameter animated: Whether the scroll should be animated. 18 | func autoScroller(didRequestAutoScroll autoScroller: PageboyAutoScroller, animated: Bool) 19 | } 20 | 21 | /// Delegate protocol for observing auto scroll events. 22 | public protocol PageboyAutoScrollerDelegate: AnyObject { 23 | 24 | /// The auto scroller will begin a scroll animation on the page view controller. 25 | /// 26 | /// - Parameter autoScroller: The auto scroller. 27 | func autoScroller(willBeginScrollAnimation autoScroller: PageboyAutoScroller) 28 | 29 | /// The auto scroller did finish a scroll animation on the page view controller. 30 | /// 31 | /// - Parameter autoScroller: The auto scroller. 32 | func autoScroller(didFinishScrollAnimation autoScroller: PageboyAutoScroller) 33 | } 34 | 35 | /// Object that provides auto scrolling framework to PageboyViewController 36 | public class PageboyAutoScroller: Any { 37 | 38 | // MARK: Types 39 | 40 | /// Duration spent on each page. 41 | /// 42 | /// - short: Short (5 seconds) 43 | /// - long: Long (10 seconds) 44 | /// - custom: Custom duration 45 | public enum IntermissionDuration { 46 | case short 47 | case long 48 | case custom(duration: TimeInterval) 49 | } 50 | 51 | // MARK: Properties 52 | 53 | /// The timer 54 | fileprivate var timer: Timer? 55 | 56 | /// Whether the auto scroller is enabled. 57 | public private(set) var isEnabled: Bool = false 58 | /// Whether the auto scroller was previously cancelled 59 | internal var wasCancelled: Bool? 60 | /// Whether a scroll animation is currently active. 61 | internal fileprivate(set) var isScrolling: Bool? 62 | 63 | /// The object that acts as a handler for auto scroll events. 64 | internal weak var handler: PageboyAutoScrollerHandler? 65 | 66 | /// The duration spent on each page during auto scrolling. Default: .short 67 | public private(set) var intermissionDuration: IntermissionDuration = .short 68 | /// Whether auto scrolling is disabled on drag of the page view controller. 69 | public var cancelsOnScroll: Bool = true 70 | /// Whether auto scrolling restarts when a page view controller scroll ends. 71 | public var restartsOnScrollEnd: Bool = false 72 | /// Whether the auto scrolling transitions should be animated. 73 | public var animateScroll: Bool = true 74 | 75 | /// The object that acts as a delegate to the auto scroller. 76 | public weak var delegate: PageboyAutoScrollerDelegate? 77 | 78 | // MARK: State 79 | 80 | /// Enable auto scrolling behaviour. 81 | /// 82 | /// - Parameter duration: The duration that should be spent on each page. 83 | public func enable(withIntermissionDuration duration: IntermissionDuration? = nil) { 84 | guard !isEnabled else { 85 | return 86 | } 87 | 88 | if let duration = duration { 89 | intermissionDuration = duration 90 | } 91 | 92 | isEnabled = true 93 | createTimer(withDuration: intermissionDuration.rawValue) 94 | } 95 | 96 | /// Disable auto scrolling behaviour 97 | public func disable() { 98 | guard isEnabled else { 99 | return 100 | } 101 | 102 | destroyTimer() 103 | isEnabled = false 104 | } 105 | 106 | /// Cancel the current auto scrolling behaviour. 107 | internal func cancel() { 108 | guard isEnabled else { 109 | return 110 | } 111 | wasCancelled = true 112 | disable() 113 | } 114 | 115 | /// Restart auto scrolling behaviour if it was previously cancelled. 116 | internal func restart() { 117 | guard wasCancelled == true && !isEnabled else { 118 | return 119 | } 120 | 121 | wasCancelled = nil 122 | enable() 123 | } 124 | 125 | /// Pause auto scrolling temporarily 126 | internal func pause() { 127 | guard isEnabled else { 128 | return 129 | } 130 | destroyTimer() 131 | } 132 | 133 | /// Resume auto scrolling if it was previously paused 134 | internal func resume() { 135 | guard isEnabled && timer == nil else { 136 | return 137 | } 138 | createTimer(withDuration: intermissionDuration.rawValue) 139 | } 140 | } 141 | 142 | // MARK: - Intervals 143 | internal extension PageboyAutoScroller.IntermissionDuration { 144 | var rawValue: TimeInterval { 145 | switch self { 146 | case .short: 147 | return 5.0 148 | case .long: 149 | return 10.0 150 | case .custom(let duration): 151 | return duration 152 | } 153 | } 154 | } 155 | 156 | // MARK: - Timer 157 | internal extension PageboyAutoScroller { 158 | 159 | /// Initialize auto scrolling timer 160 | /// 161 | /// - Parameter duration: The duration for the timer. 162 | func createTimer(withDuration duration: TimeInterval) { 163 | guard timer == nil else { 164 | return 165 | } 166 | 167 | timer = Timer.scheduledTimer(timeInterval: duration, 168 | target: self, 169 | selector: #selector(timerDidElapse(_:)), 170 | userInfo: nil, repeats: true) 171 | } 172 | 173 | /// Remove auto scrolling timer 174 | func destroyTimer() { 175 | guard timer != nil else { 176 | return 177 | } 178 | 179 | timer?.invalidate() 180 | timer = nil 181 | } 182 | 183 | /// Called when a scroll animation is finished 184 | func didFinishScrollIfEnabled() { 185 | guard isScrolling == true else { 186 | return 187 | } 188 | 189 | isScrolling = nil 190 | delegate?.autoScroller(didFinishScrollAnimation: self) 191 | } 192 | 193 | @objc func timerDidElapse(_ timer: Timer) { 194 | isScrolling = true 195 | delegate?.autoScroller(willBeginScrollAnimation: self) 196 | handler?.autoScroller(didRequestAutoScroll: self, animated: animateScroll) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Sources/Pageboy/AutoScrolling/PageboyViewController+AutoScrolling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyViewController+AutoScrolling.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 17/05/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - PageboyAutoScrollerHandler 12 | extension PageboyViewController: PageboyAutoScrollerHandler { 13 | 14 | func autoScroller(didRequestAutoScroll autoScroller: PageboyAutoScroller, animated: Bool) { 15 | scrollToPage(.next, animated: animated) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Pageboy/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Pageboy/Model/NavigationDirection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyNavigationDirection.swift 3 | // Pageboy iOS 4 | // 5 | // Created by Merrick Sapsford on 30/04/2018. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PageboyViewController { 12 | 13 | public enum NavigationDirection { 14 | case neutral 15 | case forward 16 | case reverse 17 | } 18 | } 19 | internal typealias NavigationDirection = PageboyViewController.NavigationDirection 20 | 21 | internal extension PageboyViewController.NavigationDirection { 22 | 23 | var rawValue: UIPageViewController.NavigationDirection { 24 | switch self { 25 | 26 | case .reverse: 27 | return .reverse 28 | 29 | default: 30 | return .forward 31 | } 32 | } 33 | } 34 | 35 | internal extension NavigationDirection { 36 | 37 | static func forPage(_ page: Int, 38 | previousPage: Int) -> NavigationDirection { 39 | return forPosition(CGFloat(page), previous: CGFloat(previousPage)) 40 | } 41 | 42 | static func forPosition(_ position: CGFloat, 43 | previous previousPosition: CGFloat) -> NavigationDirection { 44 | if position == previousPosition { 45 | return .neutral 46 | } 47 | return position > previousPosition ? .forward : .reverse 48 | } 49 | 50 | static func forPageScroll(to newPage: Page, 51 | at index: Int, 52 | in pageViewController: PageboyViewController) -> NavigationDirection { 53 | var direction = NavigationDirection.forPage(index, previousPage: pageViewController.currentIndex ?? index) 54 | 55 | if pageViewController.isInfiniteScrollEnabled { 56 | switch newPage { 57 | case .next: 58 | direction = .forward 59 | case .previous: 60 | direction = .reverse 61 | default: 62 | break 63 | } 64 | } 65 | 66 | return direction 67 | } 68 | } 69 | 70 | internal extension NavigationDirection { 71 | 72 | func layoutNormalized(isRtL: Bool) -> NavigationDirection { 73 | guard isRtL else { 74 | return self 75 | } 76 | return self == .forward ? .reverse : .forward 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Pageboy/Model/Page.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Page.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 30/04/2018. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension PageboyViewController { 12 | 13 | /// A page index. 14 | public typealias PageIndex = Int 15 | 16 | /// The index of a page in the page view controller. 17 | /// 18 | /// - next: The next page if available. 19 | /// - previous: The previous page if available. 20 | /// - first: The first page. 21 | /// - last: The last page. 22 | /// - at: A custom specified page index. 23 | // swiftlint:disable identifier_name 24 | public enum Page { 25 | case next 26 | case previous 27 | case first 28 | case last 29 | case at(index: PageIndex) 30 | } 31 | } 32 | internal typealias Page = PageboyViewController.Page 33 | internal typealias PageIndex = PageboyViewController.PageIndex 34 | 35 | internal extension Page { 36 | 37 | /// PageIndex value for page. 38 | /// 39 | /// - Parameter pageViewController: PageboyViewController which contains page. 40 | /// - Returns: PageIndex value. 41 | func indexValue(in pageViewController: PageboyViewController) -> PageIndex { 42 | return Page.indexValue(for: self, in: pageViewController) 43 | } 44 | 45 | /// Convert a Page to a PageIndex. 46 | /// 47 | /// - Parameters: 48 | /// - page: Page to convert. 49 | /// - pageViewController: PageboyViewController which contains page. 50 | /// - Returns: Converted PageIndex. 51 | static func indexValue(for page: Page, 52 | in pageViewController: PageboyViewController) -> PageIndex { 53 | switch page { 54 | 55 | case .next: 56 | guard let currentIndex = pageViewController.currentIndex else { 57 | return 0 58 | } 59 | var proposedIndex = currentIndex + 1 60 | if pageViewController.isInfiniteScrollEnabled && proposedIndex == pageViewController.pageCount { // scroll back to first index 61 | proposedIndex = 0 62 | } 63 | return proposedIndex 64 | 65 | case .previous: 66 | guard let currentIndex = pageViewController.currentIndex else { 67 | return 0 68 | } 69 | var proposedIndex = currentIndex - 1 70 | if pageViewController.isInfiniteScrollEnabled && proposedIndex < 0 { // scroll to last index 71 | proposedIndex = (pageViewController.pageCount ?? 1) - 1 72 | } 73 | return proposedIndex 74 | 75 | case .first: 76 | return 0 77 | 78 | case .last: 79 | return (pageViewController.pageCount ?? 1) - 1 80 | 81 | case .at(let index): 82 | return index 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Pageboy/Pageboy.h: -------------------------------------------------------------------------------- 1 | // 2 | // Pageboy.h 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 25/07/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Pageboy. 12 | FOUNDATION_EXPORT double PageboyVersionNumber; 13 | 14 | //! Project version string for Pageboy. 15 | FOUNDATION_EXPORT const unsigned char PageboyVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Pageboy/PageboyViewController+ScrollDetection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyScrollDetection.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 13/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - UIPageViewControllerDelegate 12 | extension PageboyViewController: UIPageViewControllerDelegate { 13 | 14 | // MARK: UIPageViewControllerDelegate 15 | 16 | public func pageViewController(_ pageViewController: UIPageViewController, 17 | willTransitionTo pendingViewControllers: [UIViewController]) { 18 | guard pageViewControllerIsActual(pageViewController) else { 19 | return 20 | } 21 | 22 | self.pageViewController(pageViewController, 23 | willTransitionTo: pendingViewControllers, 24 | animated: false) 25 | } 26 | 27 | internal func pageViewController(_ pageViewController: UIPageViewController, 28 | willTransitionTo pendingViewControllers: [UIViewController], 29 | animated: Bool) { 30 | guard pageViewControllerIsActual(pageViewController) else { 31 | return 32 | } 33 | 34 | guard let viewController = pendingViewControllers.first, 35 | let index = viewControllerIndexMap.index(for: viewController) else { 36 | return 37 | } 38 | 39 | expectedTransitionIndex = index 40 | let direction = NavigationDirection.forPage(index, previousPage: currentIndex ?? index) 41 | delegate?.pageboyViewController(self, 42 | willScrollToPageAt: index, 43 | direction: direction, 44 | animated: animated) 45 | } 46 | 47 | public func pageViewController(_ pageViewController: UIPageViewController, 48 | didFinishAnimating finished: Bool, 49 | previousViewControllers: [UIViewController], 50 | transitionCompleted completed: Bool) { 51 | defer { 52 | expectedTransitionIndex = nil 53 | } 54 | 55 | guard pageViewControllerIsActual(pageViewController) else { 56 | return } 57 | 58 | guard completed else { 59 | guard let expectedIndex = expectedTransitionIndex else { 60 | return } 61 | 62 | guard let viewController = previousViewControllers.first, 63 | let previousIndex = viewControllerIndexMap.index(for: viewController) else { 64 | return 65 | } 66 | 67 | delegate?.pageboyViewController(self, 68 | didCancelScrollToPageAt: expectedIndex, 69 | returnToPageAt: previousIndex) 70 | return } 71 | 72 | guard let viewController = pageViewController.viewControllers?.first, 73 | let index = viewControllerIndexMap.index(for: viewController), 74 | index == expectedTransitionIndex else { 75 | return } 76 | 77 | updateCurrentPageIndexIfNeeded(index) 78 | } 79 | } 80 | 81 | // MARK: - UIScrollViewDelegate 82 | extension PageboyViewController: UIScrollViewDelegate { 83 | 84 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 85 | guard let currentIndex = currentIndex, scrollViewIsActual(scrollView) else { 86 | return 87 | } 88 | guard updateContentOffsetForBounceIfNeeded(scrollView: scrollView) == false else { 89 | return 90 | } 91 | 92 | guard let (newPosition, previousPosition) = calculateNewPagePosition(in: scrollView, currentIndex: currentIndex) else { 93 | return 94 | } 95 | 96 | // do not continue if a page change is detected 97 | let didDetectNewPage = detectNewPageIndexIfNeeded(pagePosition: newPosition, 98 | scrollView: scrollView) 99 | guard !didDetectNewPage else { 100 | return 101 | } 102 | 103 | // update page position for infinite overscroll if required 104 | let pagePosition: CGFloat 105 | if let infiniteAdjustedPosition = adjustedPagePositionForInfiniteOverscroll(from: newPosition) { 106 | pagePosition = infiniteAdjustedPosition 107 | } else { 108 | pagePosition = newPosition 109 | } 110 | 111 | // provide scroll updates 112 | var positionPoint: CGPoint! 113 | let direction = NavigationDirection.forPosition(pagePosition, previous: previousPosition) 114 | if navigationOrientation == .horizontal { 115 | positionPoint = CGPoint(x: pagePosition, y: scrollView.contentOffset.y) 116 | } else { 117 | positionPoint = CGPoint(x: scrollView.contentOffset.x, y: pagePosition) 118 | } 119 | 120 | // ignore duplicate updates 121 | guard currentPosition != positionPoint else { 122 | return 123 | } 124 | currentPosition = positionPoint 125 | delegate?.pageboyViewController(self, 126 | didScrollTo: positionPoint, 127 | direction: direction, 128 | animated: isScrollingAnimated) 129 | previousPagePosition = pagePosition 130 | } 131 | 132 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 133 | guard scrollViewIsActual(scrollView) else { 134 | return 135 | } 136 | if autoScroller.cancelsOnScroll { 137 | autoScroller.cancel() 138 | } 139 | } 140 | 141 | public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 142 | guard scrollViewIsActual(scrollView) else { 143 | return 144 | } 145 | self.scrollView(didEndScrolling: scrollView) 146 | } 147 | 148 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 149 | guard scrollViewIsActual(scrollView) else { 150 | return 151 | } 152 | self.scrollView(didEndScrolling: scrollView) 153 | } 154 | 155 | public func scrollViewWillEndDragging(_ scrollView: UIScrollView, 156 | withVelocity velocity: CGPoint, 157 | targetContentOffset: UnsafeMutablePointer) { 158 | guard scrollViewIsActual(scrollView) else { 159 | return 160 | } 161 | 162 | updateContentOffsetForBounceIfNeeded(scrollView: scrollView) 163 | } 164 | 165 | private func scrollView(didEndScrolling scrollView: UIScrollView) { 166 | guard scrollViewIsActual(scrollView) else { 167 | return 168 | } 169 | 170 | if autoScroller.restartsOnScrollEnd { 171 | autoScroller.restart() 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/Pageboy/PageboyViewController+Updating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyViewController+Updating.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 31/03/2018. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PageboyViewController { 12 | 13 | /// Behavior to evaluate after a page update. 14 | /// 15 | /// - doNothing: Do nothing. 16 | /// - scrollToUpdate: Scroll to the update. 17 | /// - scrollTo: Scroll to a specified index. 18 | public enum PageUpdateBehavior { 19 | case doNothing 20 | case scrollToUpdate 21 | case scrollTo(index: PageIndex) 22 | } 23 | 24 | internal enum UpdateOperation { 25 | case insert 26 | case delete 27 | } 28 | } 29 | 30 | // MARK: - Page Updates 31 | internal extension PageboyViewController { 32 | 33 | func performUpdates(for newIndex: PageIndex?, 34 | viewController: UIViewController?, 35 | update: (operation: UpdateOperation, behavior: PageUpdateBehavior), 36 | indexOperation: (_ currentIndex: PageIndex, _ newIndex: PageIndex) -> Void, 37 | completion: ((Bool) -> Void)?) { 38 | guard let newIndex = newIndex, let viewController = viewController else { // no view controller - reset 39 | updateViewControllers(to: [UIViewController()], 40 | animated: false, 41 | async: false, 42 | force: false, 43 | completion: completion) 44 | self.currentIndex = nil 45 | return 46 | } 47 | 48 | guard let currentIndex = currentIndex else { // if no `currentIndex` - currently have no pages - set VC and index. 49 | updateViewControllers(to: [viewController], 50 | animated: false, 51 | async: false, 52 | force: false, 53 | completion: completion) 54 | self.currentIndex = newIndex 55 | return 56 | } 57 | 58 | // If we are inserting a page that is lower/equal to the current index 59 | // we have to move the current page up therefore we can't just cross-dissolve. 60 | let isInsertionThatRequiresMoving = update.operation == .insert && newIndex <= currentIndex 61 | 62 | if !isInsertionThatRequiresMoving && newIndex == currentIndex { // currently on the page for the update. 63 | pageViewController?.view.crossDissolve(during: { [weak self, viewController] in 64 | self?.updateViewControllers(to: [viewController], 65 | animated: false, 66 | async: true, 67 | force: false, 68 | completion: completion) 69 | }) 70 | } else { // update is happening on some other page. 71 | indexOperation(currentIndex, newIndex) 72 | 73 | // If we are deleting, check if the new index is greater than the current. If it is then we 74 | // dont need to do anything... 75 | if update.operation == .delete && newIndex > currentIndex { 76 | completion?(true) 77 | return 78 | } 79 | 80 | // Reload current view controller in UIPageViewController if insertion index is next/previous page. 81 | if pageIndex(newIndex, isNextTo: currentIndex) { 82 | 83 | let newViewController: UIViewController 84 | switch update.operation { 85 | 86 | case .insert: 87 | guard let currentViewController = currentViewController else { 88 | completion?(true) 89 | return 90 | } 91 | newViewController = currentViewController 92 | 93 | case .delete: 94 | newViewController = viewController 95 | } 96 | 97 | updateViewControllers(to: [newViewController], animated: false, async: true, force: false, completion: { [weak self, newIndex, update] _ in 98 | self?.performScrollUpdate(to: newIndex, behavior: update.behavior) 99 | completion?(true) 100 | }) 101 | } else { // Otherwise just perform scroll update 102 | performScrollUpdate(to: newIndex, behavior: update.behavior) 103 | completion?(true) 104 | } 105 | } 106 | } 107 | } 108 | 109 | // MARK: - Utilities 110 | extension PageboyViewController { 111 | 112 | func verifyNewPageCount(then update: (Int, Int) -> Void) { 113 | guard let oldPageCount = pageCount, 114 | let newPageCount = dataSource?.numberOfViewControllers(in: self) else { 115 | return 116 | } 117 | update(oldPageCount, newPageCount) 118 | } 119 | 120 | func performScrollUpdate(to update: PageIndex, behavior: PageUpdateBehavior) { 121 | switch behavior { 122 | 123 | case .scrollToUpdate: 124 | scrollToPage(.at(index: update), animated: true) 125 | 126 | case .scrollTo(let index): 127 | scrollToPage(.at(index: index), animated: true) 128 | 129 | default: 130 | break 131 | } 132 | } 133 | 134 | func pageIndex(_ index: PageIndex, isNextTo other: PageIndex) -> Bool { 135 | return index - other == 1 || other - index == 1 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/Pageboy/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyTrackingDomains 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/Pageboy/Protocols/PageboyViewControllerDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyViewControllerDataSource.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 24/11/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol PageboyViewControllerDataSource: AnyObject { 12 | 13 | /// The number of view controllers to display. 14 | /// 15 | /// - Parameter pageboyViewController: The Page view controller. 16 | /// - Returns: The total number of view controllers to display. 17 | func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int 18 | 19 | /// The view controller to display at a page index. 20 | /// 21 | /// - Parameters: 22 | /// - pageboyViewController: The Page view controller. 23 | /// - index: The page index. 24 | /// - Returns: The view controller to display 25 | func viewController(for pageboyViewController: PageboyViewController, 26 | at index: PageboyViewController.PageIndex) -> UIViewController? 27 | 28 | /// The default page index to display in the Pageboy view controller. 29 | /// 30 | /// - Parameter pageboyViewController: The Pageboy view controller 31 | /// - Returns: Default page 32 | func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Pageboy/Protocols/PageboyViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyViewControllerDelegate.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 24/11/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol PageboyViewControllerDelegate: AnyObject { 12 | 13 | /// The page view controller will begin scrolling to a new page. 14 | /// 15 | /// - Parameters: 16 | /// - pageboyViewController: The Page view controller. 17 | /// - index: The new page index. 18 | /// - direction: The direction of the scroll. 19 | /// - animation: Whether the scroll will be animated. 20 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 21 | willScrollToPageAt index: PageboyViewController.PageIndex, 22 | direction: PageboyViewController.NavigationDirection, 23 | animated: Bool) 24 | 25 | /// The page view controller did scroll to an offset between pages. 26 | /// 27 | /// - Parameters: 28 | /// - pageboyViewController: The Page view controller. 29 | /// - position: The current relative page position. 30 | /// - direction: The direction of the scroll. 31 | /// - animated: Whether the scroll is being animated. 32 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 33 | didScrollTo position: CGPoint, 34 | direction: PageboyViewController.NavigationDirection, 35 | animated: Bool) 36 | 37 | /// The page view controller did not (!) complete scroll to a new page. 38 | /// 39 | /// - Parameters: 40 | /// - pageboyViewController: The Page view controller. 41 | /// - index: The expected new page index, that was not (!) scrolled to. 42 | /// - previousIndex: The page index returned to. 43 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 44 | didCancelScrollToPageAt index: PageboyViewController.PageIndex, 45 | returnToPageAt previousIndex: PageboyViewController.PageIndex) 46 | 47 | /// The page view controller did complete scroll to a new page. 48 | /// 49 | /// - Parameters: 50 | /// - pageboyViewController: The Page view controller. 51 | /// - index: The new page index. 52 | /// - direction: The direction of the scroll. 53 | /// - animation: Whether the scroll was animated. 54 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 55 | didScrollToPageAt index: PageboyViewController.PageIndex, 56 | direction: PageboyViewController.NavigationDirection, 57 | animated: Bool) 58 | 59 | /// The page view controller did reload. 60 | /// 61 | /// - Parameters: 62 | /// - pageboyViewController: The Pageboy view controller. 63 | /// - currentViewController: The current view controller. 64 | /// - currentPageIndex: The current page index. 65 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 66 | didReloadWith currentViewController: UIViewController, 67 | currentPageIndex: PageboyViewController.PageIndex) 68 | } 69 | 70 | public extension PageboyViewControllerDelegate { 71 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 72 | didCancelScrollToPageAt index: PageboyViewController.PageIndex, 73 | returnToPageAt previousIndex: PageboyViewController.PageIndex) { 74 | // Default implementation 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Pageboy/Transitioning/PageboyViewController+Transitioning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyViewController+Transitioning.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 29/05/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - PageboyViewController transition configuration. 12 | extension PageboyViewController { 13 | 14 | /// Transition for a page scroll. 15 | public final class Transition { 16 | 17 | /// Style for the transition. 18 | /// 19 | /// - push: Slide the new page in (Default). 20 | /// - fade: Fade the new page in. 21 | /// - moveIn: Move the new page in over the top of the current page. 22 | /// - reveal: Reveal the new page under the current page. 23 | public enum Style: String { 24 | case push 25 | case fade 26 | case moveIn 27 | case reveal 28 | } 29 | 30 | /// The style for the transition. 31 | public let style: Style 32 | /// The duration of the transition. 33 | public let duration: TimeInterval 34 | 35 | // MARK: Init 36 | 37 | /// Initialize a transition. 38 | /// 39 | /// - Parameters: 40 | /// - style: The style to use. 41 | /// - duration: The duration to transition for. 42 | public init(style: Style, duration: TimeInterval) { 43 | self.style = style 44 | self.duration = duration 45 | } 46 | } 47 | } 48 | 49 | // MARK: - Custom PageboyViewController transitioning. 50 | internal extension PageboyViewController { 51 | 52 | // MARK: Set Up 53 | 54 | private func prepareForTransition() { 55 | guard transitionDisplayLink == nil else { 56 | return 57 | } 58 | 59 | let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidTick)) 60 | displayLink.isPaused = true 61 | displayLink.add(to: .main, forMode: .common) 62 | transitionDisplayLink = displayLink 63 | } 64 | 65 | private func clearUpAfterTransition() { 66 | transitionDisplayLink?.invalidate() 67 | transitionDisplayLink = nil 68 | } 69 | 70 | // MARK: Animation 71 | 72 | @objc func displayLinkDidTick() { 73 | self.activeTransitionOperation?.tick() 74 | } 75 | 76 | /// Perform a transition to a new page index. 77 | /// 78 | /// - Parameters: 79 | /// - from: The current index. 80 | /// - to: The new index. 81 | /// - direction: The direction of travel. 82 | /// - animated: Whether to animate the transition. 83 | /// - completion: Action on the completion of the transition. 84 | func performTransition(from startIndex: Int, 85 | to endIndex: Int, 86 | with direction: NavigationDirection, 87 | animated: Bool, 88 | completion: @escaping TransitionOperation.Completion) { 89 | 90 | guard let transition = transition, animated == true, activeTransitionOperation == nil else { 91 | completion(false) 92 | return 93 | } 94 | guard let scrollView = pageViewController?.scrollView else { 95 | #if DEBUG 96 | fatalError("Can't find UIPageViewController scroll view") 97 | #else 98 | return 99 | #endif 100 | } 101 | 102 | prepareForTransition() 103 | 104 | let navigationOrientation = self.navigationOrientation 105 | 106 | /// Calculate semantic direction for RtL languages 107 | var semanticDirection = direction 108 | if view.layoutIsRightToLeft && navigationOrientation == .horizontal { 109 | semanticDirection = direction.layoutNormalized(isRtL: view.layoutIsRightToLeft) 110 | } 111 | 112 | // create a transition and unpause display link 113 | let action = TransitionOperation.Action(startIndex: startIndex, 114 | endIndex: endIndex, 115 | direction: direction, 116 | semanticDirection: semanticDirection, 117 | orientation: navigationOrientation) 118 | activeTransitionOperation = TransitionOperation(for: transition, 119 | action: action, 120 | delegate: self) 121 | transitionDisplayLink?.isPaused = false 122 | 123 | // start transition 124 | activeTransitionOperation?.start(on: scrollView.layer, 125 | completion: completion) 126 | } 127 | } 128 | 129 | extension PageboyViewController: TransitionOperationDelegate { 130 | 131 | func transitionOperation(_ operation: TransitionOperation, 132 | didFinish finished: Bool) { 133 | transitionDisplayLink?.isPaused = true 134 | activeTransitionOperation = nil 135 | 136 | clearUpAfterTransition() 137 | } 138 | 139 | func transitionOperation(_ operation: TransitionOperation, 140 | didUpdateWith percentComplete: CGFloat) { 141 | 142 | let isReverse = operation.action.direction == .reverse 143 | let isVertical = operation.action.orientation == .vertical 144 | 145 | /// Take into account the diff between startIndex and endIndex 146 | let indexDiff = abs(operation.action.endIndex - operation.action.startIndex) 147 | let diff = percentComplete * CGFloat(indexDiff) 148 | 149 | let index = CGFloat(currentIndex ?? 0) 150 | let position = isReverse ? index - diff : index + diff 151 | let point = CGPoint(x: isVertical ? 0.0 : position, 152 | y: isVertical ? position : 0.0) 153 | 154 | currentPosition = point 155 | delegate?.pageboyViewController(self, didScrollTo: point, 156 | direction: operation.action.direction, 157 | animated: true) 158 | previousPagePosition = position 159 | } 160 | } 161 | 162 | internal extension CATransition { 163 | 164 | func configure(from: PageboyViewController.Transition) { 165 | duration = from.duration 166 | type = CATransitionType(rawValue: from.style.rawValue) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/Pageboy/Transitioning/TransitionOperation+Action.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionOperation+Action.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 30/05/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension TransitionOperation { 12 | 13 | /// Action that occurs in an operation. 14 | struct Action { 15 | 16 | /// The page start index. 17 | let startIndex: Int 18 | /// The page end index. 19 | let endIndex: Int 20 | /// The direction of travel. 21 | let direction: NavigationDirection 22 | /// The semantic direction of travel. In RtL languages, 23 | /// this will be the opposite of direction on the horizontal axis. 24 | let semanticDirection: NavigationDirection 25 | /// The orientation of the page view controller. 26 | let orientation: UIPageViewController.NavigationOrientation 27 | 28 | } 29 | } 30 | 31 | internal extension TransitionOperation.Action { 32 | 33 | /// Animation sub-type for the action. 34 | var transitionSubType: CATransitionSubtype { 35 | switch orientation { 36 | 37 | case .horizontal: 38 | switch semanticDirection { 39 | 40 | case .reverse: 41 | return .fromLeft 42 | default: 43 | return .fromRight 44 | } 45 | 46 | case .vertical: 47 | switch semanticDirection { 48 | 49 | case .reverse: 50 | return .fromBottom 51 | default: 52 | return .fromTop 53 | } 54 | 55 | @unknown default: 56 | fatalError("unsupported orientation \(orientation.rawValue)") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Pageboy/Transitioning/TransitionOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionOperation.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 29/05/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal protocol TransitionOperationDelegate: AnyObject { 12 | 13 | /// A transition operation did finish. 14 | /// 15 | /// - Parameters: 16 | /// - operation: The operation. 17 | /// - finished: Whether it successfully finished. 18 | func transitionOperation(_ operation: TransitionOperation, 19 | didFinish finished: Bool) 20 | 21 | /// A transition operation did progress. 22 | /// 23 | /// - Parameters: 24 | /// - operation: The operation. 25 | /// - percentComplete: The percent that the operation is complete. 26 | func transitionOperation(_ operation: TransitionOperation, 27 | didUpdateWith percentComplete: CGFloat) 28 | } 29 | 30 | /// An operation for performing a PageboyViewController transition 31 | internal class TransitionOperation: NSObject, CAAnimationDelegate { 32 | 33 | // MARK: Types 34 | 35 | /// Operation completion action. 36 | typealias Completion = (Bool) -> Void 37 | 38 | // MARK: Properties 39 | 40 | /// The transition for the operation. 41 | let transition: PageboyViewController.Transition 42 | /// The action that is occuring as part of the transition. 43 | let action: Action 44 | 45 | /// The raw animation for the operation. 46 | private var animation: CATransition? 47 | /// The time that the operation did start. 48 | private(set) var startTime: CFTimeInterval? 49 | 50 | /// Whether the operation is currently animating. 51 | private var isAnimating: Bool = false 52 | 53 | /// The object that acts as a delegate to the operation 54 | private(set) weak var delegate: TransitionOperationDelegate? 55 | /// Action to execute when the operation is complete. 56 | private var completion: Completion? 57 | 58 | /// The total duration of the transition. 59 | var duration: CFTimeInterval { 60 | guard let animation = animation else { 61 | return 0.0 62 | } 63 | return animation.duration 64 | } 65 | /// The percent that the transition is complete. 66 | var percentComplete: CGFloat { 67 | guard isAnimating else { 68 | return 0.0 69 | } 70 | 71 | let percent = CGFloat((CACurrentMediaTime() - (startTime ?? CACurrentMediaTime())) / duration) 72 | return max(0.0, min(1.0, percent)) 73 | } 74 | 75 | // MARK: Init 76 | 77 | init(for transition: PageboyViewController.Transition, 78 | action: Action, 79 | delegate: TransitionOperationDelegate) { 80 | 81 | self.action = action 82 | self.delegate = delegate 83 | self.transition = transition 84 | 85 | let animation = CATransition() 86 | animation.startProgress = 0.0 87 | animation.endProgress = 1.0 88 | animation.configure(from: transition) 89 | 90 | animation.subtype = action.transitionSubType 91 | animation.fillMode = .backwards 92 | self.animation = animation 93 | 94 | super.init() 95 | 96 | animation.delegate = self 97 | } 98 | 99 | // MARK: Transitioning 100 | 101 | /// Start the transition animation on a layer. 102 | /// 103 | /// - Parameter layer: The layer to animate. 104 | /// - Parameter completion: Completion of the transition. 105 | func start(on layer: CALayer, 106 | completion: @escaping Completion) { 107 | guard let animation = animation else { 108 | completion(false) 109 | return 110 | } 111 | 112 | self.completion = completion 113 | self.startTime = CACurrentMediaTime() 114 | layer.add(animation, 115 | forKey: "transition") 116 | } 117 | 118 | /// Perform a frame tick on the transition. 119 | func tick() { 120 | guard isAnimating else { 121 | return 122 | } 123 | delegate?.transitionOperation(self, didUpdateWith: percentComplete) 124 | } 125 | 126 | // MARK: CAAnimationDelegate 127 | 128 | public func animationDidStart(_ anim: CAAnimation) { 129 | isAnimating = true 130 | } 131 | 132 | public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { 133 | isAnimating = false 134 | completion?(flag) 135 | delegate?.transitionOperation(self, didFinish: flag) 136 | animation = nil 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/Pageboy/UIViewController+Pageboy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Pageboy.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 18/06/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | /// The parent PageboyViewController. 14 | /// Available from any direct child view controllers within a PageboyViewController. 15 | /// Deprecated in Pageboy 3.1.0. 16 | @available(*, deprecated, message: "Use pageboyParent") 17 | public var parentPageboy: PageboyViewController? { 18 | return pageboyParent 19 | } 20 | 21 | /// The parent PageboyViewController. 22 | /// Available from any direct child view controllers within a PageboyViewController. 23 | public var pageboyParent: PageboyViewController? { 24 | return parent?.parent as? PageboyViewController 25 | } 26 | 27 | /// Page index for this view controller if it's embedded in a PageboyViewController. 28 | public var pageboyPageIndex: PageboyViewController.PageIndex? { 29 | return pageboyParent?.pageIndex(of: self) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/Extensions/DispatchQueue+main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+main.swift 3 | // Pageboy 4 | // 5 | // Created by Remi Robert on 2019/02/11. 6 | // Copyright © 2019 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension DispatchQueue { 12 | 13 | static func executeInMainThread(callback: @escaping () -> Void) { 14 | if Thread.isMainThread { 15 | callback() 16 | } else { 17 | DispatchQueue.main.sync(execute: callback) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/Extensions/UIApplication+SafeShared.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+SafeShared.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 05/01/2018. 6 | // Copyright © 2018 Merrick Sapsford. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIApplication { 12 | 13 | static var safeShared: UIApplication? { 14 | guard #available(iOSApplicationExtension 8, *) else { 15 | return nil 16 | } 17 | 18 | guard UIApplication.responds(to: NSSelectorFromString("sharedApplication")), 19 | let unmanagedSharedApplication = UIApplication.perform(NSSelectorFromString("sharedApplication")) else { 20 | return nil 21 | } 22 | 23 | return unmanagedSharedApplication.takeUnretainedValue() as? UIApplication 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/Extensions/UIPageViewController+ScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIPageViewController+ScrollView.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 13/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIPageViewController { 12 | 13 | var scrollView: UIScrollView? { 14 | for subview in view.subviews { 15 | if let scrollView = subview as? UIScrollView { 16 | return scrollView 17 | } 18 | } 19 | return nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/Extensions/UIScrollView+Interaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Interaction.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 23/01/2018. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIScrollView { 12 | 13 | /// Whether the scroll view can be assumed to be interactively scrolling 14 | var isProbablyActiveInScroll: Bool { 15 | return isTracking || isDecelerating 16 | } 17 | 18 | func cancelTouches() { 19 | panGestureRecognizer.isEnabled = false 20 | panGestureRecognizer.isEnabled = true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/Extensions/UIView+Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Animation.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 26/04/2018. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | func crossDissolve(duration: TimeInterval = 0.25, 14 | during animations: @escaping () -> Void, 15 | completion: ((Bool) -> Void)? = nil) { 16 | UIView.transition(with: self, 17 | duration: duration, 18 | options: .transitionCrossDissolve, 19 | animations: animations, 20 | completion: completion) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/Extensions/UIView+AutoLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+AutoLayout.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIView { 12 | 13 | @discardableResult 14 | func pinToSuperviewEdges(priority: UILayoutPriority = .required) -> [NSLayoutConstraint] { 15 | guard let superview = guardForSuperview() else { 16 | #if DEBUG 17 | fatalError("Could not fetch superview in pinToSuperviewEdges") 18 | #else 19 | return [NSLayoutConstraint]() 20 | #endif 21 | } 22 | 23 | 24 | 25 | return addConstraints(priority: priority, { () -> [NSLayoutConstraint] in 26 | return [ 27 | topAnchor.constraint(equalTo: superview.topAnchor), 28 | leadingAnchor.constraint(equalTo: superview.leadingAnchor), 29 | bottomAnchor.constraint(equalTo: superview.bottomAnchor), 30 | trailingAnchor.constraint(equalTo: superview.trailingAnchor) 31 | ] 32 | }) 33 | } 34 | 35 | @discardableResult 36 | func matchWidth(to view: UIView, 37 | priority: UILayoutPriority = .required) -> NSLayoutConstraint? { 38 | let constraints = addConstraints(priority: priority, { () -> [NSLayoutConstraint] in 39 | return [NSLayoutConstraint(item: self, 40 | attribute: .width, 41 | relatedBy: .equal, 42 | toItem: view, 43 | attribute: .width, 44 | multiplier: 1.0, 45 | constant: 0.0)] 46 | }) 47 | 48 | guard let constraint = constraints.first else { 49 | #if DEBUG 50 | fatalError("Could not add matchWidth constraint") 51 | #else 52 | return nil 53 | #endif 54 | } 55 | return constraint 56 | } 57 | 58 | @discardableResult 59 | func matchHeight(to view: UIView, 60 | priority: UILayoutPriority = .required) -> NSLayoutConstraint? { 61 | let constraints = addConstraints(priority: priority, { () -> [NSLayoutConstraint] in 62 | return [NSLayoutConstraint(item: self, 63 | attribute: .height, 64 | relatedBy: .equal, 65 | toItem: view, 66 | attribute: .height, 67 | multiplier: 1.0, 68 | constant: 0.0)] 69 | }) 70 | 71 | guard let constraint = constraints.first else { 72 | #if DEBUG 73 | fatalError("Could not add matchHeight constraint") 74 | #else 75 | return nil 76 | #endif 77 | } 78 | return constraint 79 | } 80 | 81 | // MARK: Utilities 82 | 83 | private func prepareForAutoLayout(_ completion: () -> Void) { 84 | translatesAutoresizingMaskIntoConstraints = false 85 | completion() 86 | } 87 | 88 | @discardableResult 89 | private func addConstraints(priority: UILayoutPriority, _ completion: () -> [NSLayoutConstraint]) -> [NSLayoutConstraint] { 90 | let constraints = completion() 91 | constraints.forEach({ $0.priority = priority }) 92 | prepareForAutoLayout { 93 | NSLayoutConstraint.activate(constraints) 94 | } 95 | return constraints 96 | } 97 | 98 | private func guardForSuperview() -> UIView? { 99 | guard let superview = superview else { 100 | #if DEBUG 101 | fatalError("No superview for view \(self)") 102 | #else 103 | return nil 104 | #endif 105 | } 106 | return superview 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/Extensions/UIView+Localization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Localization.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 18/06/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | /// Whether the layout direction of the view is right to left. 14 | var layoutIsRightToLeft: Bool { 15 | var layoutDirection: UIUserInterfaceLayoutDirection! 16 | DispatchQueue.executeInMainThread { 17 | layoutDirection = self.getUserInterfaceLayoutDirection() 18 | } 19 | return layoutDirection == .rightToLeft 20 | } 21 | 22 | private func getUserInterfaceLayoutDirection() -> UIUserInterfaceLayoutDirection { 23 | return UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/IndexedObjectMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectIndexMap.swift 3 | // Pageboy iOS 4 | // 5 | // Created by Merrick Sapsford on 02/03/2019. 6 | // Copyright © 2019 UI At Six. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Map which weakly stores an object for an index key. 12 | internal final class IndexedObjectMap { 13 | 14 | // MARK: Properties 15 | 16 | private var map = [Int: WeakWrapper]() 17 | 18 | // MARK: Accessors 19 | 20 | func index(for object: T) -> Int? { 21 | cleanUp() 22 | return map.first(where: { $0.value.object === object })?.key 23 | } 24 | 25 | // MARK: Mutators 26 | 27 | func set(_ index: Int, for object: T) { 28 | cleanUp() 29 | 30 | let wrapper = WeakWrapper(object) 31 | map[index] = wrapper 32 | } 33 | 34 | func removeAll() { 35 | map.removeAll() 36 | } 37 | 38 | private func cleanUp() { 39 | var invalidIndexes = [Int]() 40 | map.forEach({ 41 | if $0.value.object == nil { 42 | invalidIndexes.append($0.key) 43 | } 44 | }) 45 | invalidIndexes.forEach({ self.map[$0] = nil }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/PatchedPageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PatchedPageViewController.swift 3 | // Pageboy 4 | // 5 | // Created by Arabia -IT on 8/25/19. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Fixes not updating dataSource on animated setViewControllers. See: https://stackoverflow.com/a/13253884/715593 11 | internal class PatchedPageViewController: UIPageViewController { 12 | 13 | private var isSettingViewControllers = false 14 | 15 | override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewController.NavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) { 16 | guard !isSettingViewControllers else { 17 | completion?(false) 18 | return 19 | } 20 | isSettingViewControllers = true 21 | super.setViewControllers(viewControllers, direction: direction, animated: animated) { (isFinished) in 22 | if isFinished && animated { 23 | DispatchQueue.main.async { 24 | super.setViewControllers(viewControllers, direction: direction, animated: false, completion: { _ in 25 | self.isSettingViewControllers = false 26 | }) 27 | } 28 | } else { 29 | self.isSettingViewControllers = false 30 | } 31 | completion?(isFinished) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Pageboy/Utilities/WeakContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakContainer.swift 3 | // Pageboy iOS 4 | // 5 | // Created by Merrick Sapsford on 02/03/2019. 6 | // Copyright © 2019 UI At Six. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal final class WeakWrapper { 12 | 13 | private(set) weak var object: T? 14 | 15 | init(_ object: T) { 16 | self.object = object 17 | } 18 | } 19 | 20 | extension WeakWrapper: Equatable { 21 | 22 | static func == (lhs: WeakWrapper, rhs: WeakWrapper) -> Bool { 23 | return lhs.object === rhs.object 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PageboyTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/PageboyTests/PageboyAutoScrollTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyAutoScrollTests.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 08/03/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pageboy 11 | 12 | class PageboyAutoScrollTests: PageboyTests { 13 | 14 | var autoScrollExpectation: XCTestExpectation? 15 | 16 | /// Test that PageboyAutoScroller enables correctly. 17 | func testAutoScrollEnabling() { 18 | self.dataSource.numberOfPages = 3 19 | self.pageboyViewController.dataSource = self.dataSource 20 | 21 | let currentIndex = self.pageboyViewController.currentIndex ?? 0 22 | let duration = self.pageboyViewController.autoScroller.intermissionDuration 23 | 24 | self.autoScrollExpectation = expectation(description: "autoScroll") 25 | 26 | self.pageboyViewController.autoScroller.animateScroll = false 27 | self.pageboyViewController.autoScroller.delegate = self 28 | self.pageboyViewController.autoScroller.enable(withIntermissionDuration: .custom(duration: 3.0)) 29 | 30 | self.waitForExpectations(timeout: duration.rawValue) { (error) in 31 | XCTAssertNil(error, "Something went wrong") 32 | XCTAssert(self.pageboyViewController.currentIndex == currentIndex + 1, 33 | "PageboyAutoScroller does not auto scroll correctly when enabled.") 34 | } 35 | } 36 | 37 | /// Test that PageboyAutoScroller disables correctly. 38 | func testAutoScrollDisable() { 39 | self.dataSource.numberOfPages = 3 40 | self.pageboyViewController.dataSource = self.dataSource 41 | 42 | self.pageboyViewController.autoScroller.enable(withIntermissionDuration: .long) 43 | self.pageboyViewController.autoScroller.disable() 44 | 45 | XCTAssert(self.pageboyViewController.autoScroller.isEnabled == false && 46 | self.pageboyViewController.autoScroller.wasCancelled != true, 47 | "PageboyAutoScroller does not disable correctly.") 48 | } 49 | 50 | /// Test that PageboyAutoScroller supports cancellation. 51 | func testAutoScrollCancellation() { 52 | self.dataSource.numberOfPages = 3 53 | self.pageboyViewController.dataSource = self.dataSource 54 | 55 | self.pageboyViewController.autoScroller.enable(withIntermissionDuration: .long) 56 | self.pageboyViewController.autoScroller.cancel() 57 | 58 | XCTAssert(self.pageboyViewController.autoScroller.isEnabled == false && 59 | self.pageboyViewController.autoScroller.wasCancelled == true, 60 | "PageboyAutoScroller does not allow cancellation correctly.") 61 | } 62 | 63 | /// Test that PageboyAutoScroller supports restarting. 64 | func testAutoScrollRestart() { 65 | self.dataSource.numberOfPages = 3 66 | self.pageboyViewController.dataSource = self.dataSource 67 | 68 | self.pageboyViewController.autoScroller.enable(withIntermissionDuration: .long) 69 | self.pageboyViewController.autoScroller.cancel() 70 | 71 | let isEnabled = self.pageboyViewController.autoScroller.isEnabled 72 | let wasEnabled = self.pageboyViewController.autoScroller.wasCancelled ?? false 73 | 74 | self.pageboyViewController.autoScroller.restart() 75 | 76 | XCTAssertTrue(self.pageboyViewController.autoScroller.isEnabled && !isEnabled && wasEnabled, 77 | "PageboyAutoScroller does not allow restarting correctly.") 78 | } 79 | } 80 | 81 | extension PageboyAutoScrollTests: PageboyAutoScrollerDelegate { 82 | 83 | func autoScroller(willBeginScrollAnimation autoScroller: PageboyAutoScroller) { 84 | 85 | } 86 | 87 | func autoScroller(didFinishScrollAnimation autoScroller: PageboyAutoScroller) { 88 | autoScrollExpectation?.fulfill() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/PageboyTests/PageboyConfigurationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyConfigurationTests.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pageboy 11 | 12 | class PageboyConfigurationTests: PageboyTests { 13 | 14 | /// Test updating navigationOrientation updates pageViewController correctly. 15 | func testPageboyNavigationOrientationChange() { 16 | self.pageboyViewController.navigationOrientation = .vertical 17 | 18 | XCTAssert(self.pageboyViewController.pageViewController?.navigationOrientation == .vertical, 19 | "Could not configure Pageboy navigationOrientation correctly") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PageboyTests/PageboyDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyDataSourceTests.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pageboy 11 | 12 | class PageboyDataSourceTests: PageboyTests { 13 | 14 | /// Test loading view controllers from the data source. 15 | func testPageboyViewControllerValidSetUp() { 16 | self.dataSource.numberOfPages = 1 17 | self.pageboyViewController.dataSource = self.dataSource 18 | 19 | XCTAssert(self.pageboyViewController.pageCount == 1, 20 | "View Controllers were not successfully loaded from the data source.") 21 | } 22 | 23 | /// Test loading an empty view controller array from the data source. 24 | func testPageboyViewControllerEmptySetUp() { 25 | self.dataSource.numberOfPages = 0 26 | self.pageboyViewController.dataSource = self.dataSource 27 | 28 | XCTAssert(self.pageboyViewController.pageCount == 0, 29 | "Empty view controller array not successfully loaded from the data source.") 30 | } 31 | 32 | /// Test loading a nil array from the data source. 33 | func testPageboyViewControllerNilSetUp() { 34 | self.pageboyViewController.dataSource = self.dataSource 35 | 36 | XCTAssertNil(self.pageboyViewController.currentViewController, 37 | "Current view controller is not nil when data source returns nil.") 38 | } 39 | 40 | /// Test using the default .first PageIndex when nil returned 41 | /// in defaultPageIndex(forPageboyViewController:). 42 | func testPageboyViewControllerDefaultPageIndexDefault() { 43 | self.dataSource.numberOfPages = 3 44 | self.pageboyViewController.dataSource = self.dataSource 45 | 46 | XCTAssert(self.pageboyViewController.currentIndex == 0, 47 | "Default Page index is not using correct .first PageIndex when no value is returned.") 48 | } 49 | 50 | /// Test using a custom PageIndex when returned 51 | /// in defaultPageIndex(forPageboyViewController:). 52 | func testPageboyViewControllerDefaultPageIndexCustom() { 53 | self.dataSource.numberOfPages = 3 54 | self.dataSource.defaultIndex = .at(index: 1) 55 | self.pageboyViewController.dataSource = self.dataSource 56 | 57 | XCTAssert(self.pageboyViewController.currentIndex == 1, 58 | "Default page index is not using correct index when specified.") 59 | } 60 | 61 | /// Test using a custom out of range PageIndex when returned 62 | /// in defaultPageIndex(forPageboyViewController:). 63 | func testPageboyViewControllerDefaultPageIndexOutOfRange() { 64 | self.dataSource.numberOfPages = 3 65 | self.dataSource.defaultIndex = .at(index: 4) 66 | self.pageboyViewController.dataSource = self.dataSource 67 | 68 | XCTAssertNil(self.pageboyViewController.currentIndex, 69 | "Default page index is not correctly erroring when out of range index specified.") 70 | } 71 | 72 | /// Test using an invalid .next PageIndex when returned 73 | /// in defaultPageIndex(forPageboyViewController:). 74 | func testPageboyViewControllerDefaultPageIndexInvalid() { 75 | self.dataSource.numberOfPages = 3 76 | self.dataSource.defaultIndex = .next 77 | self.pageboyViewController.dataSource = self.dataSource 78 | 79 | XCTAssert(self.pageboyViewController.currentIndex == 0, 80 | "Default page index is not correctly handling an invalid index specified.") 81 | } 82 | 83 | /// Test using .first PageIndex when returned 84 | /// in defaultPageIndex(forPageboyViewController:). 85 | func testPageboyViewControllerDefaultPageIndexFirst() { 86 | self.dataSource.numberOfPages = 3 87 | self.dataSource.defaultIndex = .first 88 | self.pageboyViewController.dataSource = self.dataSource 89 | 90 | XCTAssert(self.pageboyViewController.currentIndex == 0, 91 | "Default Page index is not using correct .first PageIndex when specified.") 92 | } 93 | 94 | /// Test using .last PageIndex when returned 95 | /// in defaultPageIndex(forPageboyViewController:). 96 | func testPageboyViewControllerDefaultPageIndexLast() { 97 | self.dataSource.numberOfPages = 3 98 | self.dataSource.defaultIndex = .last 99 | self.pageboyViewController.dataSource = self.dataSource 100 | 101 | XCTAssert(self.pageboyViewController.currentIndex == 2, 102 | "Default Page index is not using correct .last PageIndex when specified.") 103 | } 104 | 105 | /// Test whether reloadPages fully reloads 106 | /// PageboyViewController. 107 | func testPageboyViewControllerReloadBehavior() { 108 | self.dataSource.numberOfPages = 5 109 | self.pageboyViewController.dataSource = self.dataSource 110 | let initialPageCount = self.pageboyViewController.pageCount 111 | 112 | self.dataSource.numberOfPages = 3 113 | self.pageboyViewController.reloadData() 114 | let reloadPageCount = self.pageboyViewController.pageCount 115 | 116 | XCTAssert(initialPageCount == 5 && reloadPageCount == 3, 117 | "reloadPages is not correctly reloading view controllers.") 118 | } 119 | 120 | /// Test that reloadPages successfully calls 121 | /// appropriate delegate function. 122 | func testPageboyViewControllerReloadDelegate() { 123 | self.dataSource.numberOfPages = 5 124 | self.pageboyViewController.dataSource = self.dataSource 125 | 126 | self.dataSource.numberOfPages = 3 127 | self.pageboyViewController.reloadData() 128 | 129 | let reloadPageCount = self.delegate.lastDidReloadPageCount 130 | 131 | XCTAssertTrue(reloadPageCount == 3, 132 | "reloadPages does not call didReloadViewControllers delegate function.") 133 | } 134 | 135 | /// Test that reloadCurrentPageSoftly does not cause a data source reload. 136 | func testPageboyViewControllerSoftReloadBehavior() { 137 | self.dataSource.numberOfPages = 5 138 | self.pageboyViewController.dataSource = self.dataSource 139 | 140 | self.pageboyViewController.isInfiniteScrollEnabled = true 141 | 142 | XCTAssertTrue(self.delegate.reloadCount == 1, 143 | "reloadCurrentPageSoftly causes the data source to reload") 144 | } 145 | 146 | /// Test that the UIPageViewController data source is 147 | /// returning correct pageViewController:viewControllerAfter: values. 148 | func testPageViewControllerDataSourceNextController() { 149 | self.dataSource.numberOfPages = 3 150 | self.pageboyViewController.dataSource = self.dataSource 151 | 152 | let viewController = self.dataSource.viewControllers![0] 153 | let nextViewController = self.pageboyViewController.pageViewController(self.pageboyViewController.pageViewController!, 154 | viewControllerAfter: viewController) 155 | 156 | XCTAssert(nextViewController === self.dataSource.viewControllers?[1], 157 | "pageViewController:viewControllerAfter is returning an incorrect view controller") 158 | } 159 | 160 | /// Test that the UIPageViewController data source is 161 | /// returning nil from pageViewController:viewControllerAfter: at the end of the pages. 162 | func testPageViewControllerDataSourceNilNextController() { 163 | self.dataSource.numberOfPages = 3 164 | self.dataSource.defaultIndex = .last 165 | self.pageboyViewController.dataSource = self.dataSource 166 | 167 | let viewController = self.dataSource.viewControllers![2] 168 | let nextViewController = self.pageboyViewController.pageViewController(self.pageboyViewController.pageViewController!, 169 | viewControllerAfter: viewController) 170 | 171 | XCTAssertNil(nextViewController, 172 | "pageViewController:viewControllerAfter is returning an incorrect view controller when at the end of pages.") 173 | } 174 | 175 | /// Test that the UIPageViewController data source is 176 | /// returning correct pageViewController:viewControllerBefore: values. 177 | func testPageViewControllerDataSourcePreviousController() { 178 | self.dataSource.numberOfPages = 3 179 | self.dataSource.defaultIndex = .at(index: 1) 180 | self.pageboyViewController.dataSource = self.dataSource 181 | 182 | let viewController = self.dataSource.viewControllers![1] 183 | let previousViewController = self.pageboyViewController.pageViewController(self.pageboyViewController.pageViewController!, 184 | viewControllerBefore: viewController) 185 | 186 | XCTAssert(previousViewController === self.dataSource.viewControllers?[0], 187 | "pageViewController:viewControllerBefore is returning an incorrect view controller") 188 | } 189 | 190 | /// Test that the UIPageViewController data source is 191 | /// returning nil from pageViewController:viewControllerBefore: at the start of the pages. 192 | func testPageViewControllerDataSourceNilPreviousController() { 193 | self.dataSource.numberOfPages = 3 194 | self.pageboyViewController.dataSource = self.dataSource 195 | 196 | let viewController = self.dataSource.viewControllers![0] 197 | let previousViewController = self.pageboyViewController.pageViewController(self.pageboyViewController.pageViewController!, 198 | viewControllerBefore: viewController) 199 | 200 | XCTAssertNil(previousViewController, 201 | "pageViewController:viewControllerBefore is returning an incorrect view controller when at the start of pages.") 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Sources/PageboyTests/PageboyInsertionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyInsertionTests.swift 3 | // PageboyTests 4 | // 5 | // Created by Merrick Sapsford on 13/11/2018. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pageboy 11 | 12 | class PageboyInsertionTests: PageboyTests { 13 | 14 | func testInsertPage() { 15 | let initialCount = 4 16 | dataSource.numberOfPages = initialCount 17 | pageboyViewController.dataSource = dataSource 18 | 19 | // Insert 20 | let index = 0 21 | let viewController = dataSource.generateViewControllers(count: 1)[index] 22 | dataSource.viewControllers?.insert(viewController, at: index) 23 | pageboyViewController.insertPage(at: index, then: .doNothing) 24 | 25 | XCTAssert(pageboyViewController.pageCount == initialCount + 1) 26 | } 27 | 28 | func testDeletePage() { 29 | let initialCount = 5 30 | dataSource.numberOfPages = initialCount 31 | pageboyViewController.dataSource = dataSource 32 | 33 | let index = 2 34 | dataSource.viewControllers?.remove(at: index) 35 | pageboyViewController.deletePage(at: index, then: .doNothing) 36 | 37 | XCTAssert(pageboyViewController.pageCount == initialCount - 1) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PageboyTests/PageboyPropertyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyPropertyTests.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 22/03/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pageboy 11 | 12 | class PageboyPropertyTests: PageboyTests { 13 | 14 | /// Test that currentViewController property returns correct view controller. 15 | func testCorrectCurrentViewControllerReported() { 16 | self.dataSource.numberOfPages = 5 17 | self.pageboyViewController.dataSource = self.dataSource 18 | 19 | performAsyncTest { (completion) in 20 | 21 | self.pageboyViewController.scrollToPage(.next, animated: false) { (newViewController, animated, finished) in 22 | let currentViewController = self.pageboyViewController.currentViewController 23 | 24 | XCTAssertTrue(currentViewController === self.dataSource.viewControllers?[1], 25 | "currentViewController property is incorrect following transitions.") 26 | completion() 27 | } 28 | } 29 | } 30 | 31 | /// Test that setting isScrollEnabled updates internal scroll view correctly. 32 | func testIsScrollEnabledUpdates() { 33 | self.dataSource.numberOfPages = 5 34 | self.pageboyViewController.dataSource = self.dataSource 35 | 36 | self.pageboyViewController.isScrollEnabled = false 37 | 38 | XCTAssertTrue(self.pageboyViewController.pageViewController?.scrollView?.isScrollEnabled == false, 39 | "isScrollEnabled does not update the internal scrollView correctly.") 40 | } 41 | 42 | // func testPageViewControllerOptions() { 43 | // self.dataSource.numberOfPages = 5 44 | // self.pageboyViewController.dataSource = self.dataSource 45 | // 46 | // self.pageboyViewController.interPageSpacing = 12.0 47 | // 48 | // XCTAssert(self.pageboyViewController.pageViewControllerOptions?.count ?? 0 > 0, 49 | // "Custom UIPageViewController options are not being passed.") 50 | // } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/PageboyTests/PageboyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyTests.swift 3 | // PageboyTests 4 | // 5 | // Created by Merrick Sapsford on 04/01/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pageboy 11 | 12 | class PageboyTests: XCTestCase { 13 | 14 | typealias AsyncTest = (@escaping TestCompletion) -> Void 15 | typealias TestCompletion = () -> Void 16 | 17 | var pageboyViewController: TestPageBoyViewController! 18 | var dataSource: TestPageboyDataSource! 19 | var delegate: TestPageboyDelegate! 20 | 21 | private var expectations = [XCTestExpectation]() 22 | 23 | // MARK: Environment 24 | 25 | override func setUp() { 26 | super.setUp() 27 | 28 | pageboyViewController = TestPageBoyViewController() 29 | dataSource = TestPageboyDataSource() 30 | delegate = TestPageboyDelegate() 31 | 32 | pageboyViewController.delegate = delegate 33 | 34 | let bounds = UIScreen.main.bounds 35 | pageboyViewController.view.frame = bounds 36 | 37 | pageboyViewController.loadViewIfNeeded() 38 | } 39 | 40 | // MARK: Tests 41 | 42 | private func testInit() { 43 | XCTAssert(pageboyViewController != nil, 44 | "PageBoyViewController initialization failed") 45 | } 46 | 47 | func performAsyncTest(timeout: TimeInterval = 0.3, 48 | test: AsyncTest) { 49 | let exp = expectation(description: "Async test") 50 | let index = expectations.count 51 | expectations.append(exp) 52 | test { [unowned self] in 53 | exp.fulfill() 54 | self.expectations.remove(at: index) 55 | } 56 | waitForExpectations(timeout: timeout) { (error) in 57 | guard let error = error else { 58 | return 59 | } 60 | 61 | XCTFail("Expectation failed with \(error)") 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/PageboyTests/PageboyTransitionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyTransitionTests.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pageboy 11 | 12 | class PageboyTransitionTests: PageboyTests { 13 | 14 | /// Test transition to a valid custom PageIndex non-animated 15 | func testSuccessfulTransitionToCustomIndex() { 16 | self.dataSource.numberOfPages = 5 17 | self.pageboyViewController.dataSource = self.dataSource 18 | let transitionIndex = 2 19 | 20 | performAsyncTest { (completion) in 21 | self.pageboyViewController.scrollToPage(.at(index: transitionIndex), 22 | animated: false) 23 | { (newViewController, animated, finished) in 24 | XCTAssert(self.pageboyViewController.currentIndex == transitionIndex, 25 | "Not transitioning to valid custom index correctly (Non animated).") 26 | completion() 27 | } 28 | } 29 | } 30 | 31 | /// Test attempting transition to an out of bounds custom PageIndex 32 | func testHandlingTransitionToOutOfBoundsCustomIndex() { 33 | self.dataSource.numberOfPages = 5 34 | self.pageboyViewController.dataSource = self.dataSource 35 | let transitionIndex = 6 36 | 37 | self.pageboyViewController.scrollToPage(.at(index: transitionIndex), animated: false) 38 | 39 | XCTAssert(self.pageboyViewController.currentIndex == 0, 40 | "Not handling out of bounds transition index request correctly.") 41 | } 42 | 43 | /// Test transition to a .first PageIndex 44 | func testSuccessfulTransitionToFirstIndex() { 45 | self.dataSource.numberOfPages = 5 46 | self.pageboyViewController.dataSource = self.dataSource 47 | 48 | self.pageboyViewController.scrollToPage(.first, animated: false) 49 | 50 | XCTAssert(self.pageboyViewController.currentIndex == 0, 51 | "Not transitioning to first index correctly.") 52 | } 53 | 54 | /// Test transition to a .last PageIndex 55 | func testSuccessfulTransitionToLastIndex() { 56 | self.dataSource.numberOfPages = 5 57 | self.pageboyViewController.dataSource = self.dataSource 58 | 59 | performAsyncTest { (completion) in 60 | self.pageboyViewController.scrollToPage(.last, 61 | animated: false) 62 | { (newViewController, animated, finished) in 63 | XCTAssert(self.pageboyViewController.currentIndex == 4, 64 | "Not transitioning to last index correctly.") 65 | completion() 66 | } 67 | } 68 | } 69 | 70 | /// Test transition to a .next PageIndex 71 | func testSuccessfulTransitionToNextIndex() { 72 | self.dataSource.numberOfPages = 5 73 | self.pageboyViewController.dataSource = self.dataSource 74 | 75 | performAsyncTest { (completion) in 76 | self.pageboyViewController.scrollToPage(.next, 77 | animated: false) 78 | { (newViewController, animated, finished) in 79 | XCTAssert(self.pageboyViewController.currentIndex == 1, 80 | "Not transitioning to next index correctly.") 81 | completion() 82 | } 83 | } 84 | } 85 | 86 | /// Test transition to a .previous PageIndex 87 | func testSuccessfulTransitionToPreviousIndex() { 88 | self.dataSource.numberOfPages = 5 89 | self.pageboyViewController.dataSource = self.dataSource 90 | 91 | performAsyncTest { (completion) in 92 | 93 | self.pageboyViewController.scrollToPage(.last, 94 | animated: false) 95 | { (newViewController, animated, finished) in 96 | self.pageboyViewController.scrollToPage(.previous, 97 | animated: false) 98 | { (newViewController, animated, finished) in 99 | XCTAssert(self.pageboyViewController.currentIndex == 3, 100 | "Not transitioning to previous index correctly.") 101 | completion() 102 | } 103 | } 104 | } 105 | } 106 | 107 | /// Test partial user interacted transition reports offsets correctly. 108 | func testPartialTransitionOffsetReporting() { 109 | self.dataSource.numberOfPages = 5 110 | self.pageboyViewController.dataSource = self.dataSource 111 | 112 | // simulate scroll 113 | self.simulateScroll(toPosition: 0.5) 114 | 115 | XCTAssert(String(format:"%.1f", self.delegate.lastRecordedPagePosition?.x ?? 0.0) == "0.5" && 116 | self.pageboyViewController.currentIndex == 0, 117 | "Not reporting partial user interacted transition offset values correctly.") 118 | 119 | } 120 | 121 | /// Test partial user interacted transition reports direction correctly. 122 | func testPartialTransitionDirectionReporting() { 123 | self.dataSource.numberOfPages = 5 124 | self.pageboyViewController.dataSource = self.dataSource 125 | 126 | // simulate scroll 127 | self.simulateScroll(toPosition: 0.5) 128 | 129 | XCTAssert(self.delegate.lastRecordedDirection == .forward && self.pageboyViewController.currentIndex == 0, 130 | "Not reporting partial user interacted transition direction values correctly.") 131 | } 132 | 133 | /// Test animated flags are correct for non-animated transitions. 134 | func testNonAnimatedTransitionAnimatedFlags() { 135 | self.dataSource.numberOfPages = 5 136 | self.pageboyViewController.dataSource = self.dataSource 137 | let transitionIndex = 3 138 | 139 | self.pageboyViewController.scrollToPage(.at(index: transitionIndex), animated: false) 140 | { (newViewController, animated, finished) in 141 | 142 | XCTAssert(self.pageboyViewController.currentIndex == transitionIndex && 143 | self.delegate.lastWillScrollToPageAnimated == false && 144 | self.delegate.lastDidScrollToPageAtIndexAnimated == false && 145 | self.delegate.lastDidScrollToPositionAnimated == false, 146 | "Animated flags for an non animated scrollToPage are incorrect.") 147 | } 148 | } 149 | 150 | /// Test bounces flag is correctly adhered to when set to false. 151 | func testBouncingDisabledTransition() { 152 | self.dataSource.numberOfPages = 2 153 | self.pageboyViewController.dataSource = self.dataSource 154 | self.pageboyViewController.bounces = false 155 | 156 | self.simulateScroll(toPosition: -0.1) 157 | 158 | XCTAssert(self.pageboyViewController.currentPosition!.x == 0.0, 159 | "Bounces flag is not adhered to when setting contentOffset when false.") 160 | } 161 | 162 | // MARK: Utils 163 | 164 | func simulateScroll(toPosition position: CGFloat) { 165 | let targetIndex = Int(position.rounded()) 166 | 167 | let boundsWidth = self.pageboyViewController.view.frame.size.width 168 | self.pageboyViewController.expectedTransitionIndex = targetIndex 169 | self.pageboyViewController.pageViewController?.scrollView?.setContentOffset(CGPoint(x: boundsWidth + (boundsWidth * position), y: 0.0), 170 | animated: false) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/PageboyTests/TestComponents/TestPageChildViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestPageChildViewController.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestPageChildViewController: UIViewController { 12 | 13 | var index: Int? 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PageboyTests/TestComponents/TestPageboyDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestPageboyDataSource.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import Pageboy 12 | 13 | class TestPageboyDataSource: PageboyViewControllerDataSource { 14 | 15 | var numberOfPages: Int? { 16 | didSet { 17 | guard let numberOfPages = numberOfPages else { 18 | self.viewControllers = nil 19 | return 20 | } 21 | self.viewControllers = generateViewControllers(count: numberOfPages) 22 | } 23 | } 24 | var defaultIndex: PageboyViewController.Page? 25 | var viewControllers: [UIViewController]? 26 | 27 | // MARK: PageboyViewControllerDataSource 28 | 29 | func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { 30 | return viewControllers?.count ?? 0 31 | } 32 | 33 | func viewController(for pageboyViewController: PageboyViewController, 34 | at index: PageboyViewController.PageIndex) -> UIViewController? { 35 | return viewControllers?[index] 36 | } 37 | 38 | func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { 39 | return defaultIndex 40 | } 41 | 42 | // MARK: Utility 43 | 44 | func generateViewControllers(count: Int) -> [UIViewController] { 45 | var viewControllers = [UIViewController]() 46 | 47 | for index in 0 ..< count { 48 | 49 | let viewController = TestPageChildViewController() 50 | viewController.index = index 51 | viewControllers.append(viewController) 52 | } 53 | 54 | return viewControllers 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/PageboyTests/TestComponents/TestPageboyDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestPageboyDelegate.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import Pageboy 12 | 13 | class TestPageboyDelegate: PageboyViewControllerDelegate { 14 | 15 | var lastRecordedPagePosition: CGPoint? 16 | var lastRecordedPageIndex: Int? 17 | var lastRecordedDirection: PageboyViewController.NavigationDirection? 18 | 19 | var lastWillScrollToPageAnimated: Bool? 20 | var lastDidScrollToPositionAnimated: Bool? 21 | var lastDidScrollToPageAtIndexAnimated: Bool? 22 | 23 | var lastDidReloadPageCount: Int? 24 | var lastDidReloadCurrentViewController: UIViewController? 25 | var lastDidReloadCurrentIndex: PageboyViewController.PageIndex? 26 | var reloadCount: Int = 0 27 | 28 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 29 | willScrollToPageAt index: PageboyViewController.PageIndex, 30 | direction: PageboyViewController.NavigationDirection, 31 | animated: Bool) { 32 | lastWillScrollToPageAnimated = animated 33 | } 34 | 35 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 36 | didScrollTo position: CGPoint, 37 | direction: PageboyViewController.NavigationDirection, 38 | animated: Bool) { 39 | lastDidScrollToPositionAnimated = animated 40 | lastRecordedPagePosition = position 41 | lastRecordedDirection = direction 42 | } 43 | 44 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 45 | didScrollToPageAt index: PageboyViewController.PageIndex, 46 | direction: PageboyViewController.NavigationDirection, 47 | animated: Bool) { 48 | lastDidScrollToPageAtIndexAnimated = animated 49 | lastRecordedPageIndex = index 50 | lastRecordedDirection = direction 51 | } 52 | 53 | func pageboyViewController(_ pageboyViewController: PageboyViewController, 54 | didReloadWith currentViewController: UIViewController, 55 | currentPageIndex: PageboyViewController.PageIndex) { 56 | lastDidReloadPageCount = pageboyViewController.pageCount 57 | lastDidReloadCurrentViewController = currentViewController 58 | lastDidReloadCurrentIndex = currentPageIndex 59 | 60 | reloadCount += 1 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/PageboyTests/TestComponents/TestPageboyViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestPageboyViewController.swift 3 | // Pageboy 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | class TestPageBoyViewController: PageboyViewController { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Shared/GradientBackgroundViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientBackgroundViewController.swift 3 | // Example 4 | // 5 | // Created by Merrick Sapsford on 04/10/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | final class GradientBackgroundViewController: UIViewController { 11 | 12 | // MARK: Properties 13 | 14 | private lazy var gradientView = GradientView(colors: colors) 15 | let child: UIViewController 16 | 17 | let colors: [UIColor] 18 | 19 | // MARK: Init 20 | 21 | init(embedding child: UIViewController, colors: [UIColor]) { 22 | self.child = child 23 | self.colors = colors 24 | super.init(nibName: nil, bundle: nil) 25 | } 26 | 27 | @available(*, unavailable) 28 | required init?(coder: NSCoder) { 29 | fatalError("Not Supported") 30 | } 31 | 32 | // MARK: Lifecycle 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | view.addSubview(gradientView) 38 | gradientView.translatesAutoresizingMaskIntoConstraints = false 39 | NSLayoutConstraint.activate([ 40 | gradientView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 41 | gradientView.topAnchor.constraint(equalTo: view.topAnchor), 42 | view.trailingAnchor.constraint(equalTo: gradientView.trailingAnchor), 43 | view.bottomAnchor.constraint(equalTo: gradientView.bottomAnchor) 44 | ]) 45 | 46 | addChild(child) 47 | view.addSubview(child.view) 48 | child.view.translatesAutoresizingMaskIntoConstraints = false 49 | NSLayoutConstraint.activate([ 50 | child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 51 | child.view.topAnchor.constraint(equalTo: view.topAnchor), 52 | view.trailingAnchor.constraint(equalTo: child.view.trailingAnchor), 53 | view.bottomAnchor.constraint(equalTo: child.view.bottomAnchor) 54 | ]) 55 | } 56 | } 57 | 58 | private final class GradientView: UIView { 59 | 60 | // MARK: Properties 61 | 62 | private var gradientLayer: CAGradientLayer? { 63 | if let gradientLayer = self.layer as? CAGradientLayer { 64 | return gradientLayer 65 | } 66 | return nil 67 | } 68 | 69 | var colors: [UIColor]? { 70 | didSet { 71 | var colorRefs = [CGColor]() 72 | for color in colors ?? [] { 73 | colorRefs.append(color.cgColor) 74 | } 75 | gradientLayer?.colors = colorRefs 76 | } 77 | } 78 | 79 | var locations: [Double]? { 80 | didSet { 81 | var locationNumbers = [NSNumber]() 82 | for location in locations ?? [] { 83 | locationNumbers.append(NSNumber(value: location)) 84 | } 85 | gradientLayer?.locations = locationNumbers 86 | } 87 | } 88 | 89 | var startPoint: CGPoint = CGPoint(x: 0.5, y: 0.0) { 90 | didSet { 91 | gradientLayer?.startPoint = startPoint 92 | } 93 | } 94 | 95 | var endPoint: CGPoint = CGPoint(x: 0.5, y: 1.0) { 96 | didSet { 97 | gradientLayer?.endPoint = endPoint 98 | } 99 | } 100 | 101 | override class var layerClass: AnyClass { 102 | return CAGradientLayer.self 103 | } 104 | 105 | // MARK: Init 106 | 107 | init(colors: [UIColor]) { 108 | super.init(frame: .zero) 109 | commonInit(colors: colors) 110 | } 111 | 112 | override init(frame: CGRect) { 113 | super.init(frame: frame) 114 | commonInit() 115 | } 116 | 117 | required init?(coder aDecoder: NSCoder) { 118 | super.init(coder: aDecoder) 119 | commonInit() 120 | } 121 | 122 | private func commonInit(colors: [UIColor] = [UIColor.white, UIColor.black]) { 123 | self.colors = colors 124 | gradientLayer?.startPoint = self.startPoint 125 | gradientLayer?.endPoint = self.endPoint 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/Shared/PageboyStatusView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyStatusView.swift 3 | // Examples 4 | // 5 | // Created by Merrick Sapsford on 10/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | class PageboyStatusView: UIView { 13 | 14 | // MARK: Properties 15 | 16 | private let divider = UIView() 17 | private let countLabel = UILabel() 18 | private let positionLabel = UILabel() 19 | private let pageLabel = UILabel() 20 | 21 | override var tintColor: UIColor! { 22 | didSet { 23 | updateForTintColor() 24 | } 25 | } 26 | 27 | // MARK: Init 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | commonInit() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | commonInit() 37 | } 38 | 39 | private func commonInit() { 40 | 41 | let stackView = UIStackView() 42 | stackView.axis = .vertical 43 | 44 | addSubview(divider) 45 | addSubview(stackView) 46 | divider.translatesAutoresizingMaskIntoConstraints = false 47 | stackView.translatesAutoresizingMaskIntoConstraints = false 48 | NSLayoutConstraint.activate([ 49 | divider.leadingAnchor.constraint(equalTo: leadingAnchor), 50 | divider.topAnchor.constraint(equalTo: topAnchor), 51 | divider.widthAnchor.constraint(equalToConstant: 1.0), 52 | bottomAnchor.constraint(equalTo: divider.bottomAnchor), 53 | stackView.leadingAnchor.constraint(equalTo: divider.trailingAnchor, constant: 8.0), 54 | stackView.topAnchor.constraint(equalTo: topAnchor), 55 | trailingAnchor.constraint(equalTo: stackView.trailingAnchor), 56 | bottomAnchor.constraint(equalTo: stackView.bottomAnchor) 57 | ]) 58 | 59 | stackView.addArrangedSubview(countLabel) 60 | stackView.addArrangedSubview(positionLabel) 61 | stackView.addArrangedSubview(pageLabel) 62 | 63 | updateCount(nil) 64 | updatePosition(nil) 65 | updatePage(nil) 66 | 67 | tintColor = UIColor.white.withAlphaComponent(0.75) 68 | 69 | switch traitCollection.userInterfaceIdiom { 70 | case .tv: 71 | countLabel.font = .systemFont(ofSize: 18) 72 | positionLabel.font = .systemFont(ofSize: 18) 73 | pageLabel.font = .systemFont(ofSize: 18) 74 | default: 75 | countLabel.font = .systemFont(ofSize: 14) 76 | positionLabel.font = .systemFont(ofSize: 14) 77 | pageLabel.font = .systemFont(ofSize: 14) 78 | } 79 | 80 | updateForTintColor() 81 | } 82 | 83 | // MARK: Styling 84 | 85 | private func updateForTintColor() { 86 | divider.backgroundColor = tintColor 87 | countLabel.textColor = tintColor 88 | positionLabel.textColor = tintColor 89 | pageLabel.textColor = tintColor 90 | } 91 | 92 | // MARK: Data 93 | 94 | private func updateCount(_ count: Int?) { 95 | countLabel.text = "Page Count: \(count ?? 0)" 96 | } 97 | 98 | private func updatePosition(_ position: CGFloat?) { 99 | positionLabel.text = "Current Position: \(String(format: "%.3f", position ?? 0.0))" 100 | } 101 | 102 | private func updatePage(_ page: Int?) { 103 | pageLabel.text = "Current Page: \(page ?? 0)" 104 | } 105 | } 106 | 107 | extension PageboyStatusView: PageboyViewControllerDelegate { 108 | 109 | func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollTo position: CGPoint, direction: PageboyViewController.NavigationDirection, animated: Bool) { 110 | switch pageboyViewController.navigationOrientation { 111 | case .horizontal: 112 | updatePosition(position.x) 113 | case .vertical: 114 | updatePosition(position.y) 115 | @unknown default: 116 | break 117 | } 118 | } 119 | 120 | func pageboyViewController(_ pageboyViewController: PageboyViewController, willScrollToPageAt index: PageboyViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) { 121 | 122 | } 123 | 124 | func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: PageboyViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) { 125 | updatePage(index) 126 | } 127 | 128 | func pageboyViewController(_ pageboyViewController: PageboyViewController, didCancelScrollToPageAt index: PageboyViewController.PageIndex, returnToPageAt previousIndex: PageboyViewController.PageIndex) { 129 | 130 | } 131 | 132 | func pageboyViewController(_ pageboyViewController: PageboyViewController, didReloadWith currentViewController: UIViewController, currentPageIndex: PageboyViewController.PageIndex) { 133 | updateCount(pageboyViewController.pageCount) 134 | } 135 | } 136 | 137 | extension PageboyStatusView { 138 | 139 | class func add(to viewController: PageboyViewController) { 140 | 141 | let statusView = PageboyStatusView() 142 | viewController.delegate = statusView 143 | 144 | viewController.view.addSubview(statusView) 145 | statusView.translatesAutoresizingMaskIntoConstraints = false 146 | 147 | NSLayoutConstraint.activate([ 148 | statusView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor, constant: 16.0), 149 | viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 8.0) 150 | ]) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/Shared/UIColor+Pageboy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Pageboy.swift 3 | // Example 4 | // 5 | // Created by Merrick Sapsford on 04/10/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | 12 | class var pageboyPrimary: UIColor { 13 | UIColor(red: 0.01, green: 0.00, blue: 0.18, alpha: 1.0) 14 | } 15 | 16 | class var pageboySecondary: UIColor { 17 | UIColor(red: 0.00, green: 0.53, blue: 0.80, alpha: 1.0) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS 4 | // 5 | // Created by Merrick Sapsford on 04/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | let gradientColors: [UIColor] = [.pageboyPrimary, .pageboySecondary] 20 | 21 | let pageViewController = PageViewController() 22 | PageboyStatusView.add(to: pageViewController) 23 | let navigationController = NavigationController(navigationBarClass: TransparentNavigationBar.self, toolbarClass: nil) 24 | navigationController.viewControllers = [pageViewController] 25 | 26 | window = UIWindow(frame: UIScreen.main.bounds) 27 | window?.rootViewController = GradientBackgroundViewController(embedding: navigationController, colors: gradientColors) 28 | window?.makeKeyAndVisible() 29 | 30 | return true 31 | } 32 | } 33 | 34 | @available(iOS 13, *) 35 | extension AppDelegate { 36 | 37 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 38 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.424", 9 | "green" : "0.090", 10 | "red" : "0.059" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "Icon@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "idiom" : "ipad", 47 | "size" : "20x20", 48 | "scale" : "1x" 49 | }, 50 | { 51 | "idiom" : "ipad", 52 | "size" : "20x20", 53 | "scale" : "2x" 54 | }, 55 | { 56 | "idiom" : "ipad", 57 | "size" : "29x29", 58 | "scale" : "1x" 59 | }, 60 | { 61 | "idiom" : "ipad", 62 | "size" : "29x29", 63 | "scale" : "2x" 64 | }, 65 | { 66 | "idiom" : "ipad", 67 | "size" : "40x40", 68 | "scale" : "1x" 69 | }, 70 | { 71 | "idiom" : "ipad", 72 | "size" : "40x40", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "76x76", 77 | "idiom" : "ipad", 78 | "filename" : "IconPad.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "76x76", 83 | "idiom" : "ipad", 84 | "filename" : "IconPad@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "83.5x83.5", 89 | "idiom" : "ipad", 90 | "filename" : "IconPadPro.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "1024x1024", 95 | "idiom" : "ios-marketing", 96 | "filename" : "Icon.png", 97 | "scale" : "1x" 98 | } 99 | ], 100 | "info" : { 101 | "version" : 1, 102 | "author" : "xcode" 103 | } 104 | } -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AppIcon.appiconset/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/AppIcon.appiconset/Icon@2x.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AppIcon.appiconset/Icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/AppIcon.appiconset/Icon@3x.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AppIcon.appiconset/IconPad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/AppIcon.appiconset/IconPad.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AppIcon.appiconset/IconPad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/AppIcon.appiconset/IconPad@2x.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/AppIcon.appiconset/IconPadPro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/AppIcon.appiconset/IconPadPro.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/bg_logo_launch.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bg_logo_launch.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "bg_logo_launch@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "bg_logo_launch@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/bg_logo_launch.imageset/bg_logo_launch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/bg_logo_launch.imageset/bg_logo_launch.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/bg_logo_launch.imageset/bg_logo_launch@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/bg_logo_launch.imageset/bg_logo_launch@2x.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/bg_logo_launch.imageset/bg_logo_launch@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/bg_logo_launch.imageset/bg_logo_launch@3x.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_minus.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_minus.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_minus.imageset/ic_minus.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/ic_minus.imageset/ic_minus.pdf -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_plus.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_plus.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_plus.imageset/ic_plus.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/ic_plus.imageset/ic_plus.pdf -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_welcome_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_welcome_icon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_welcome_icon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_welcome_icon@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_welcome_icon.imageset/ic_welcome_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/ic_welcome_icon.imageset/ic_welcome_icon.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_welcome_icon.imageset/ic_welcome_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/ic_welcome_icon.imageset/ic_welcome_icon@2x.png -------------------------------------------------------------------------------- /Sources/iOS/Assets.xcassets/ic_welcome_icon.imageset/ic_welcome_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/iOS/Assets.xcassets/ic_welcome_icon.imageset/ic_welcome_icon@3x.png -------------------------------------------------------------------------------- /Sources/iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Sources/iOS/ChildViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChildViewController.swift 3 | // Example 4 | // 5 | // Created by Merrick Sapsford on 04/10/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | class ChildViewController: UIViewController { 11 | 12 | // MARK: Properties 13 | 14 | let page: Int 15 | 16 | override var preferredStatusBarStyle: UIStatusBarStyle { 17 | .lightContent 18 | } 19 | 20 | // MARK: Init 21 | 22 | init(page: Int) { 23 | self.page = page 24 | super.init(nibName: nil, bundle: nil) 25 | } 26 | 27 | @available(*, unavailable) 28 | required init?(coder: NSCoder) { 29 | fatalError("Not supported") 30 | } 31 | 32 | // MARK: Lifecycle 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | let label = UILabel() 38 | view.addSubview(label) 39 | label.translatesAutoresizingMaskIntoConstraints = false 40 | NSLayoutConstraint.activate([ 41 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor), 42 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor) 43 | ]) 44 | label.text = "Page \(page)" 45 | label.font = .systemFont(ofSize: 20.0, weight: .medium) 46 | label.textColor = .lightText 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/iOS/Example iOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/iOS/Extras/GradientBackgroundViewController+Appearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientBackgroundViewController+Appearance.swift 3 | // Example iOS 4 | // 5 | // Created by Merrick Sapsford on 10/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension GradientBackgroundViewController { 12 | 13 | override var childForStatusBarStyle: UIViewController? { 14 | child 15 | } 16 | 17 | override var childForStatusBarHidden: UIViewController? { 18 | child 19 | } 20 | 21 | override var childForHomeIndicatorAutoHidden: UIViewController? { 22 | child 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/iOS/Extras/NavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationController.swift 3 | // Example iOS 4 | // 5 | // Created by Merrick Sapsford on 10/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NavigationController: UINavigationController { 12 | 13 | open override var childForStatusBarStyle: UIViewController? { 14 | topViewController 15 | } 16 | 17 | open override var childForStatusBarHidden: UIViewController? { 18 | topViewController 19 | } 20 | 21 | open override var childForHomeIndicatorAutoHidden: UIViewController? { 22 | topViewController 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/iOS/Extras/Pageboy+NavigationNotifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pageboy+NavigationNotifications.swift 3 | // Example iOS 4 | // 5 | // Created by Merrick Sapsford on 11/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | extension Notification.Name { 13 | 14 | static let nextPage = Notification.Name("com.uias.Pageboy.nextPage") 15 | static let previousPage = Notification.Name("com.uias.Pageboy.previousPage") 16 | } 17 | 18 | extension PageboyViewController { 19 | 20 | func registerForNavigationNotifications() { 21 | NotificationCenter.default.addObserver(self, selector: #selector(nextPage(_:)), name: .nextPage, object: nil) 22 | NotificationCenter.default.addObserver(self, selector: #selector(previousPage(_:)), name: .previousPage, object: nil) 23 | } 24 | 25 | @objc private func nextPage(_ sender: Notification) { 26 | scrollToPage(.next, animated: true) 27 | } 28 | 29 | @objc private func previousPage(_ sender: Notification) { 30 | scrollToPage(.previous, animated: true) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/iOS/Extras/PageboyTouchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageboyTouchBar.swift 3 | // Example iOS 4 | // 5 | // Created by Merrick Sapsford on 11/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | #if targetEnvironment(macCatalyst) 13 | 14 | extension NSTouchBarItem.Identifier { 15 | static let nextPage = NSTouchBarItem.Identifier("com.uias.Pageboy.nextPage") 16 | static let previousPage = NSTouchBarItem.Identifier("com.uias.Pageboy.previousPage") 17 | } 18 | 19 | extension PageViewController: NSTouchBarDelegate { 20 | 21 | open override func makeTouchBar() -> NSTouchBar? { 22 | let touchBar = NSTouchBar() 23 | touchBar.delegate = self 24 | 25 | touchBar.defaultItemIdentifiers = [ 26 | .previousPage, 27 | .nextPage, 28 | .flexibleSpace 29 | ] 30 | 31 | return touchBar 32 | } 33 | 34 | public func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { 35 | let touchBarItem: NSTouchBarItem? 36 | 37 | switch identifier { 38 | 39 | case .previousPage: 40 | guard let image = UIImage(systemName: "chevron.left") else { 41 | return nil 42 | } 43 | touchBarItem = NSButtonTouchBarItem(identifier: identifier, 44 | image: image, 45 | target: self, 46 | action: #selector(PageViewController.scrollToPreviousPage(_:))) 47 | 48 | case .nextPage: 49 | guard let image = UIImage(systemName: "chevron.right") else { 50 | return nil 51 | } 52 | touchBarItem = NSButtonTouchBarItem(identifier: identifier, 53 | image: image, 54 | target: self, 55 | action: #selector(PageViewController.scrollToNextPage(_:))) 56 | 57 | default: 58 | touchBarItem = nil 59 | } 60 | 61 | return touchBarItem 62 | } 63 | } 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/iOS/Extras/ToolbarDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolbarDelegate.swift 3 | // Example iOS 4 | // 5 | // Created by Merrick Sapsford on 11/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ToolbarDelegate: NSObject { 12 | 13 | } 14 | 15 | #if targetEnvironment(macCatalyst) 16 | 17 | extension NSToolbarItem.Identifier { 18 | static let nextPage = NSToolbarItem.Identifier("com.uias.Pageboy.nextPage") 19 | static let previousPage = NSToolbarItem.Identifier("com.uias.Pageboy.previousPage") 20 | } 21 | 22 | extension ToolbarDelegate { 23 | 24 | @objc func nextPage(_ sender: Any?) { 25 | NotificationCenter.default.post(Notification(name: .nextPage)) 26 | } 27 | 28 | @objc func previousPage(_ sender: Any?) { 29 | NotificationCenter.default.post(Notification(name: .previousPage)) 30 | } 31 | } 32 | 33 | extension ToolbarDelegate: NSToolbarDelegate { 34 | 35 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 36 | let identifiers: [NSToolbarItem.Identifier] = [ 37 | .previousPage, 38 | .nextPage 39 | ] 40 | return identifiers 41 | } 42 | 43 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 44 | return toolbarDefaultItemIdentifiers(toolbar) 45 | } 46 | 47 | func toolbar(_ toolbar: NSToolbar, 48 | itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, 49 | willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { 50 | 51 | var toolbarItem: NSToolbarItem? 52 | 53 | switch itemIdentifier { 54 | 55 | case .nextPage: 56 | let item = NSToolbarItem(itemIdentifier: itemIdentifier) 57 | item.image = UIImage(systemName: "chevron.right") 58 | item.label = "Next Page" 59 | item.action = #selector(nextPage(_:)) 60 | item.target = self 61 | toolbarItem = item 62 | 63 | case .previousPage: 64 | let item = NSToolbarItem(itemIdentifier: itemIdentifier) 65 | item.image = UIImage(systemName: "chevron.left") 66 | item.label = "Previous Page" 67 | item.action = #selector(previousPage(_:)) 68 | item.target = self 69 | toolbarItem = item 70 | 71 | default: 72 | toolbarItem = nil 73 | } 74 | 75 | return toolbarItem 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/iOS/Extras/TransparentNavigationBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransparentNavigationBar.swift 3 | // Example iOS 4 | // 5 | // Created by Merrick Sapsford on 15/02/2017. 6 | // Copyright © 2022 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TransparentNavigationBar: UINavigationBar { 12 | 13 | private let separatorView = UIView() 14 | 15 | // MARK: Init 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | commonInit() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | commonInit() 25 | } 26 | 27 | private func commonInit() { 28 | var titleTextAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white] 29 | titleTextAttributes[.font] = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.semibold) 30 | self.titleTextAttributes = titleTextAttributes 31 | self.tintColor = UIColor.white.withAlphaComponent(0.7) 32 | 33 | self.setBackgroundImage(UIImage(), for: .default) 34 | self.shadowImage = UIImage() 35 | self.isTranslucent = true 36 | 37 | separatorView.backgroundColor = UIColor.white.withAlphaComponent(0.5) 38 | self.addSubview(separatorView) 39 | separatorView.frame = CGRect(x: 0.0, 40 | y: self.bounds.size.height - 1.0, 41 | width: self.bounds.size.width, height: 0.5) 42 | } 43 | 44 | // MARK: Lifecycle 45 | 46 | override func layoutSubviews() { 47 | super.layoutSubviews() 48 | 49 | separatorView.frame = CGRect(x: 0.0, 50 | y: self.bounds.size.height - 1.0, 51 | width: self.bounds.size.width, height: 0.5) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIViewControllerBasedStatusBarAppearance 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Pageboy 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UIApplicationSupportsIndirectInputEvents 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIApplicationSceneManifest 49 | 50 | UIApplicationSupportsMultipleScenes 51 | 52 | UISceneConfigurations 53 | 54 | UIWindowSceneSessionRoleApplication 55 | 56 | 57 | UISceneConfigurationName 58 | Default Configuration 59 | UISceneDelegateClassName 60 | $(PRODUCT_MODULE_NAME).SceneDelegate 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Sources/iOS/PageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageViewController.swift 3 | // Example 4 | // 5 | // Created by Merrick Sapsford on 04/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | class PageViewController: PageboyViewController, PageboyViewControllerDataSource { 13 | 14 | // MARK: Properties 15 | 16 | /// View controllers that will be displayed in page view controller. 17 | private lazy var viewControllers: [UIViewController] = { 18 | (1 ... 10).map({ ChildViewController(page: $0) }) 19 | }() 20 | 21 | // MARK: Lifecycle 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | // Set PageboyViewControllerDataSource dataSource to configure page view controller. 27 | dataSource = self 28 | 29 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(scrollToNextPage(_:))) 30 | navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Previous", style: .plain, target: self, action: #selector(scrollToPreviousPage(_:))) 31 | } 32 | 33 | // MARK: PageboyViewControllerDataSource 34 | 35 | func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { 36 | viewControllers.count // How many view controllers to display in the page view controller. 37 | } 38 | 39 | func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? { 40 | viewControllers[index] // View controller to display at a specific index for the page view controller. 41 | } 42 | 43 | func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { 44 | nil // Default page to display in the page view controller (nil equals default/first index). 45 | } 46 | 47 | // MARK: Actions 48 | 49 | @objc func scrollToNextPage(_ sender: UIBarButtonItem) { 50 | scrollToPage(.next, animated: true) 51 | } 52 | 53 | @objc func scrollToPreviousPage(_ sender: UIBarButtonItem) { 54 | scrollToPage(.previous, animated: true) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/iOS/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Example iOS 4 | // 5 | // Created by Merrick Sapsford on 11/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // swiftlint:disable weak_delegate 12 | 13 | @available(iOS 13, *) 14 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 15 | 16 | var window: UIWindow? 17 | var toolbarDelegate = ToolbarDelegate() 18 | 19 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 20 | guard let windowScene = scene as? UIWindowScene else { 21 | return 22 | } 23 | 24 | let window = UIWindow(windowScene: windowScene) 25 | 26 | let pageViewController = PageViewController() 27 | PageboyStatusView.add(to: pageViewController) 28 | 29 | #if targetEnvironment(macCatalyst) 30 | 31 | let toolbar = NSToolbar(identifier: "main") 32 | toolbar.delegate = toolbarDelegate 33 | toolbar.displayMode = .iconOnly 34 | 35 | if let titlebar = windowScene.titlebar { 36 | titlebar.toolbar = toolbar 37 | if #available(iOS 14.0, *) { 38 | titlebar.toolbarStyle = .automatic 39 | } 40 | } 41 | 42 | pageViewController.registerForNavigationNotifications() 43 | let contentViewController = pageViewController 44 | 45 | #else 46 | let navigationController = NavigationController(navigationBarClass: TransparentNavigationBar.self, toolbarClass: nil) 47 | navigationController.viewControllers = [pageViewController] 48 | let contentViewController = navigationController 49 | #endif 50 | 51 | let gradientColors: [UIColor] = [.pageboyPrimary, .pageboySecondary] 52 | window.rootViewController = GradientBackgroundViewController(embedding: contentViewController, colors: gradientColors) 53 | 54 | self.window = window 55 | window.makeKeyAndVisible() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/tvOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // tvOS 4 | // 5 | // Created by Merrick Sapsford on 10/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | @main 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | let gradientColors: [UIColor] = [.pageboyPrimary, .pageboySecondary] 20 | 21 | let pageViewController = PageViewController() 22 | addStatusView(to: pageViewController) 23 | 24 | window = UIWindow(frame: UIScreen.main.bounds) 25 | window?.rootViewController = GradientBackgroundViewController(embedding: pageViewController, colors: gradientColors) 26 | window?.makeKeyAndVisible() 27 | 28 | return true 29 | } 30 | 31 | func applicationWillResignActive(_ application: UIApplication) { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 34 | } 35 | 36 | func applicationDidEnterBackground(_ application: UIApplication) { 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | } 39 | 40 | func applicationWillEnterForeground(_ application: UIApplication) { 41 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 42 | } 43 | 44 | func applicationDidBecomeActive(_ application: UIApplication) { 45 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 46 | } 47 | } 48 | 49 | extension AppDelegate { 50 | 51 | private func addStatusView(to viewController: PageboyViewController) { 52 | 53 | let statusView = PageboyStatusView() 54 | viewController.delegate = statusView 55 | 56 | viewController.view.addSubview(statusView) 57 | statusView.translatesAutoresizingMaskIntoConstraints = false 58 | NSLayoutConstraint.activate([ 59 | statusView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor, constant: 32.0), 60 | viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 8.0) 61 | ]) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Background.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Background.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Foreground.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Foreground.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Middle.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Background.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Background@2x.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Background.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Background@2x.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Foreground.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Foreground@2x.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Foreground.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Foreground@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Foreground@2x.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Middle.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Middle@2x.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uias/Pageboy/3f4df3cc962cb61af5d1c0f744db77af5a9ed937/Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/tvOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/tvOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/tvOS/ChildViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChildViewController.swift 3 | // Example tvOS 4 | // 5 | // Created by Merrick Sapsford on 10/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ChildViewController: UIViewController { 12 | 13 | // MARK: Properties 14 | 15 | let page: Int 16 | 17 | // MARK: Init 18 | 19 | init(page: Int) { 20 | self.page = page 21 | super.init(nibName: nil, bundle: nil) 22 | } 23 | 24 | @available(*, unavailable) 25 | required init?(coder: NSCoder) { 26 | fatalError("Not supported") 27 | } 28 | 29 | // MARK: Lifecycle 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | let label = UILabel() 35 | view.addSubview(label) 36 | label.translatesAutoresizingMaskIntoConstraints = false 37 | NSLayoutConstraint.activate([ 38 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor), 39 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor) 40 | ]) 41 | label.text = "Page \(page)" 42 | label.font = .systemFont(ofSize: 32.0, weight: .medium) 43 | label.textColor = .white 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Pageboy 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | arm64 30 | 31 | UIUserInterfaceStyle 32 | Automatic 33 | 34 | 35 | -------------------------------------------------------------------------------- /Sources/tvOS/PageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageViewController.swift 3 | // Example tvOS 4 | // 5 | // Created by Merrick Sapsford on 10/10/2020. 6 | // Copyright © 2020 UI At Six. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Pageboy 11 | 12 | class PageViewController: PageboyViewController, PageboyViewControllerDataSource { 13 | 14 | // MARK: Properties 15 | 16 | /// View controllers that will be displayed in page view controller. 17 | private lazy var viewControllers: [UIViewController] = [ 18 | ChildViewController(page: 1), 19 | ChildViewController(page: 2), 20 | ChildViewController(page: 3), 21 | ChildViewController(page: 4), 22 | ChildViewController(page: 5) 23 | ] 24 | 25 | // MARK: Lifecycle 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | // Set PageboyViewControllerDataSource dataSource to configure page view controller. 31 | dataSource = self 32 | } 33 | 34 | // MARK: PageboyViewControllerDataSource 35 | 36 | func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { 37 | viewControllers.count // How many view controllers to display in the page view controller. 38 | } 39 | 40 | func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? { 41 | viewControllers[index] // View controller to display at a specific index for the page view controller. 42 | } 43 | 44 | func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { 45 | nil // Default page to display in the page view controller (nil equals default/first index). 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier "com.uias.pageboy" # The bundle identifier of your app 2 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | 2 | fastlane_version "2.26.1" 3 | 4 | default_platform :ios 5 | 6 | platform :ios do 7 | 8 | desc "Run unit tests and check library" 9 | lane :test do 10 | scan(workspace: "Pageboy.xcworkspace", scheme: "Pageboy iOS", clean: true) 11 | pod_lib_lint(allow_warnings: true) 12 | end 13 | 14 | desc "Deploy a new version to CocoaPods and GitHub" 15 | lane :deploy do 16 | test 17 | 18 | podspec = "Pageboy.podspec" 19 | version = version_get_podspec(path: podspec) 20 | 21 | # Push new Github release 22 | github_release = set_github_release( 23 | repository_name: "uias/Pageboy", 24 | api_token: ENV["GITHUB_API_TOKEN"], 25 | name: version, 26 | tag_name: version, 27 | description: "#{version} release.", 28 | commitish: "main" 29 | ) 30 | 31 | # Push spec 32 | pod_push(allow_warnings: true, verbose: true) 33 | 34 | slack( 35 | message: "Pageboy v#{version} released!" 36 | ) 37 | end 38 | end --------------------------------------------------------------------------------