├── .swift-version ├── ExampleSwiftUI ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── DefaultView.swift ├── ExampleApp.swift ├── SelectedIndexView.swift ├── ChangeItems.swift ├── LifecycleView.swift ├── Info.plist └── Base.lproj │ └── LaunchScreen.storyboard ├── Example ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── Header │ │ │ ├── Contents.json │ │ │ └── Header.imageset │ │ │ │ ├── Header.jpg │ │ │ │ └── Contents.json │ │ ├── Icons │ │ │ ├── Contents.json │ │ │ ├── axe.imageset │ │ │ │ ├── axe.pdf │ │ │ │ └── Contents.json │ │ │ ├── map.imageset │ │ │ │ ├── map.pdf │ │ │ │ └── Contents.json │ │ │ ├── sun.imageset │ │ │ │ ├── sun.pdf │ │ │ │ └── Contents.json │ │ │ ├── moon.imageset │ │ │ │ ├── moon.pdf │ │ │ │ └── Contents.json │ │ │ ├── snow.imageset │ │ │ │ ├── snow.pdf │ │ │ │ └── Contents.json │ │ │ ├── star.imageset │ │ │ │ ├── star.pdf │ │ │ │ └── Contents.json │ │ │ ├── tipi.imageset │ │ │ │ ├── tipi.pdf │ │ │ │ └── Contents.json │ │ │ ├── tree.imageset │ │ │ │ ├── tree.pdf │ │ │ │ └── Contents.json │ │ │ ├── wind.imageset │ │ │ │ ├── wind.pdf │ │ │ │ └── Contents.json │ │ │ ├── wood.imageset │ │ │ │ ├── wood.pdf │ │ │ │ └── Contents.json │ │ │ ├── bonnet.imageset │ │ │ │ ├── bonnet.pdf │ │ │ │ └── Contents.json │ │ │ ├── cloud.imageset │ │ │ │ ├── cloud.pdf │ │ │ │ └── Contents.json │ │ │ ├── earth.imageset │ │ │ │ ├── earth.pdf │ │ │ │ └── Contents.json │ │ │ ├── knife.imageset │ │ │ │ ├── knife.pdf │ │ │ │ └── Contents.json │ │ │ ├── leave.imageset │ │ │ │ ├── leave.pdf │ │ │ │ └── Contents.json │ │ │ ├── light.imageset │ │ │ │ ├── light.pdf │ │ │ │ └── Contents.json │ │ │ ├── shoes.imageset │ │ │ │ ├── shoes.pdf │ │ │ │ └── Contents.json │ │ │ ├── water.imageset │ │ │ │ ├── water.pdf │ │ │ │ └── Contents.json │ │ │ ├── compass.imageset │ │ │ │ ├── compass.pdf │ │ │ │ └── Contents.json │ │ │ └── mushroom.imageset │ │ │ │ ├── mushroom.pdf │ │ │ │ └── Contents.json │ │ ├── Unsplash │ │ │ ├── Contents.json │ │ │ ├── city-1.imageset │ │ │ │ ├── city-1.jpeg │ │ │ │ └── Contents.json │ │ │ ├── city-2.imageset │ │ │ │ ├── city-2.jpeg │ │ │ │ └── Contents.json │ │ │ ├── city-3.imageset │ │ │ │ ├── city-3.jpeg │ │ │ │ └── Contents.json │ │ │ ├── city-4.imageset │ │ │ │ ├── city-4.jpeg │ │ │ │ └── Contents.json │ │ │ ├── food-1.imageset │ │ │ │ ├── food-1.jpeg │ │ │ │ └── Contents.json │ │ │ ├── food-2.imageset │ │ │ │ ├── food-2.jpeg │ │ │ │ └── Contents.json │ │ │ ├── food-3.imageset │ │ │ │ ├── food-3.jpeg │ │ │ │ └── Contents.json │ │ │ ├── food-4.imageset │ │ │ │ ├── food-4.jpeg │ │ │ │ └── Contents.json │ │ │ ├── green-1.imageset │ │ │ │ ├── green-1.jpeg │ │ │ │ └── Contents.json │ │ │ ├── green-2.imageset │ │ │ │ ├── green-2.jpeg │ │ │ │ └── Contents.json │ │ │ ├── green-3.imageset │ │ │ │ ├── green-3.jpeg │ │ │ │ └── Contents.json │ │ │ ├── green-4.imageset │ │ │ │ ├── green-4.jpeg │ │ │ │ └── Contents.json │ │ │ ├── coffee-1.imageset │ │ │ │ ├── coffee-1.jpeg │ │ │ │ └── Contents.json │ │ │ ├── coffee-2.imageset │ │ │ │ ├── coffee-2.jpeg │ │ │ │ └── Contents.json │ │ │ ├── coffee-3.imageset │ │ │ │ ├── coffee-3.jpeg │ │ │ │ └── Contents.json │ │ │ ├── coffee-4.imageset │ │ │ │ ├── coffee-4.jpeg │ │ │ │ └── Contents.json │ │ │ ├── scenic-1.imageset │ │ │ │ ├── scenic-1.jpeg │ │ │ │ └── Contents.json │ │ │ ├── scenic-2.imageset │ │ │ │ ├── scenic-2.jpeg │ │ │ │ └── Contents.json │ │ │ ├── scenic-3.imageset │ │ │ │ ├── scenic-3.jpeg │ │ │ │ └── Contents.json │ │ │ ├── scenic-4.imageset │ │ │ │ ├── scenic-4.jpeg │ │ │ │ └── Contents.json │ │ │ ├── succulents-1.imageset │ │ │ │ ├── succulents-1.jpeg │ │ │ │ └── Contents.json │ │ │ ├── succulents-2.imageset │ │ │ │ ├── succulents-2.jpeg │ │ │ │ └── Contents.json │ │ │ ├── succulents-3.imageset │ │ │ │ ├── succulents-3.jpeg │ │ │ │ └── Contents.json │ │ │ └── succulents-4.imageset │ │ │ │ ├── succulents-4.jpeg │ │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── TableViewController.swift │ ├── UIColor+interpolation.swift │ ├── ContentViewController.swift │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── UIView+constraints.swift ├── AppDelegate.swift ├── Examples │ ├── Calendar │ │ ├── DateFormatters.swift │ │ ├── CalendarViewController.swift │ │ └── CalendarPagingCell.swift │ ├── Icons │ │ ├── IconViewController.swift │ │ ├── IconsViewController.swift │ │ └── IconPagingCell.swift │ ├── Images │ │ ├── ImageCollectionViewCell.swift │ │ ├── ImagePagingCell.swift │ │ └── ImagesViewController.swift │ ├── Storyboard │ │ └── StoryboardViewController.swift │ ├── SelfSizing │ │ └── SelfSizingViewController.swift │ ├── Basic │ │ └── BasicViewController.swift │ ├── MultipleCells │ │ └── MultipleCellsViewController.swift │ ├── NavigationBar │ │ └── NavigationBarViewController.swift │ ├── PageViewController │ │ └── PageViewExampleViewController.swift │ └── SizeDelegate │ │ └── SizeDelegateViewController.swift └── ExamplesViewController.swift ├── ParchmentTests ├── Resources.xcassets │ ├── Contents.json │ └── Green.imageset │ │ ├── Green.png │ │ └── Contents.json ├── Mocks │ ├── MockPagingControllerSizeDelegate.swift │ ├── MockPageViewManagerDataSource.swift │ ├── MockPagingControllerDataSource.swift │ ├── MockPagingControllerDelegate.swift │ ├── Mock.swift │ ├── MockCollectionViewLayout.swift │ ├── MockPageViewManagerDelegate.swift │ └── MockCollectionView.swift ├── Item.swift ├── UIColorInterpolationTests.swift ├── Info.plist ├── PagingViewTests.swift ├── PagingDataStructureTests.swift ├── PagingIndicatorLayoutAttributesTests.swift ├── Utilities │ └── CreateDistance.swift └── PagingStateTests.swift ├── Parchment ├── Enums │ ├── PagingMenuPosition.swift │ ├── PagingContentInteraction.swift │ ├── PagingMenuInteraction.swift │ ├── PagingBorderOptions.swift │ ├── PagingIndicatorOptions.swift │ ├── PagingMenuHorizontalAlignment.swift │ ├── PagingMenuTransition.swift │ ├── PagingSelectedScrollPosition.swift │ ├── PageViewState.swift │ ├── PagingDirection.swift │ ├── PagingMenuItemSource.swift │ ├── PageViewDirection.swift │ ├── PagingMenuItemSize.swift │ ├── InvalidationState.swift │ └── PagingState.swift ├── Structs │ ├── PagingNavigationOrientation.swift │ ├── PagingTransition.swift │ ├── AnyPagingItem.swift │ ├── PagingIndexItem.swift │ ├── PagingCellViewModel.swift │ ├── PagingIndicatorMetric.swift │ ├── PagingDiff.swift │ └── PagingItems.swift ├── Protocols │ ├── Tween.swift │ ├── PagingMenuDelegate.swift │ ├── PagingMenuDataSource.swift │ ├── PageViewManagerDataSource.swift │ ├── PagingLayout.swift │ ├── PagingViewControllerSizeDelegate.swift │ ├── PagingItem.swift │ ├── PageViewControllerDataSource.swift │ ├── PageViewManagerDelegate.swift │ ├── PagingViewControllerDataSource.swift │ ├── PagingViewControllerInfiniteDataSource.swift │ ├── PageViewControllerDelegate.swift │ ├── CollectionView.swift │ └── PagingViewControllerDelegate.swift ├── Classes │ ├── PagingInvalidationContext.swift │ ├── PagingBorderView.swift │ ├── PagingIndicatorView.swift │ ├── PagingCellLayoutAttributes.swift │ ├── PagingCell.swift │ ├── PagingFiniteDataSource.swift │ ├── PagingStaticDataSource.swift │ ├── PagingIndicatorLayoutAttributes.swift │ ├── PagingBorderLayoutAttributes.swift │ ├── PagingSizeCache.swift │ ├── PagingView.swift │ └── PagingTitleCell.swift ├── Extensions │ ├── UIEdgeInsets.swift │ ├── UIColor+interpolation.swift │ └── UIView+constraints.swift ├── Parchment.h └── Resources │ └── Info.plist ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Parchment.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Parchment.xcscheme ├── .gitignore ├── Package.swift ├── .circleci └── config.yml ├── ParchmentUITests ├── Info.plist └── ParchmentUITests.swift ├── Parchment.podspec.json ├── LICENSE └── Documentation ├── basic-usage.md ├── data-source.md └── infinite-data-source.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /ExampleSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Header/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ParchmentTests/Resources.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ExampleSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Parchment/Enums/PagingMenuPosition.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PagingMenuPosition { 4 | case top // default 5 | case bottom 6 | } 7 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingContentInteraction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PagingContentInteraction { 4 | case scrolling 5 | case none 6 | } 7 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | } 7 | -------------------------------------------------------------------------------- /ParchmentTests/Resources.xcassets/Green.imageset/Green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/ParchmentTests/Resources.xcassets/Green.imageset/Green.png -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/axe.imageset/axe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/axe.imageset/axe.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/map.imageset/map.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/map.imageset/map.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/sun.imageset/sun.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/sun.imageset/sun.pdf -------------------------------------------------------------------------------- /Parchment/Enums/PagingMenuInteraction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PagingMenuInteraction { 4 | case scrolling 5 | case swipe 6 | case none 7 | } 8 | -------------------------------------------------------------------------------- /Parchment/Structs/PagingNavigationOrientation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PagingNavigationOrientation { 4 | case vertical 5 | case horizontal 6 | } 7 | -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/moon.imageset/moon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/moon.imageset/moon.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/snow.imageset/snow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/snow.imageset/snow.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/star.imageset/star.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/star.imageset/star.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/tipi.imageset/tipi.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/tipi.imageset/tipi.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/tree.imageset/tree.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/tree.imageset/tree.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/wind.imageset/wind.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/wind.imageset/wind.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/wood.imageset/wood.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/wood.imageset/wood.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/bonnet.imageset/bonnet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/bonnet.imageset/bonnet.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/cloud.imageset/cloud.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/cloud.imageset/cloud.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/earth.imageset/earth.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/earth.imageset/earth.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/knife.imageset/knife.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/knife.imageset/knife.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/leave.imageset/leave.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/leave.imageset/leave.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/light.imageset/light.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/light.imageset/light.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/shoes.imageset/shoes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/shoes.imageset/shoes.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/water.imageset/water.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/water.imageset/water.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Header/Header.imageset/Header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Header/Header.imageset/Header.jpg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/compass.imageset/compass.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/compass.imageset/compass.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/mushroom.imageset/mushroom.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Icons/mushroom.imageset/mushroom.pdf -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-1.imageset/city-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/city-1.imageset/city-1.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-2.imageset/city-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/city-2.imageset/city-2.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-3.imageset/city-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/city-3.imageset/city-3.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-4.imageset/city-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/city-4.imageset/city-4.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-1.imageset/food-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/food-1.imageset/food-1.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-2.imageset/food-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/food-2.imageset/food-2.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-3.imageset/food-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/food-3.imageset/food-3.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-4.imageset/food-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/food-4.imageset/food-4.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-1.imageset/green-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/green-1.imageset/green-1.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-2.imageset/green-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/green-2.imageset/green-2.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-3.imageset/green-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/green-3.imageset/green-3.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-4.imageset/green-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/green-4.imageset/green-4.jpeg -------------------------------------------------------------------------------- /Parchment/Protocols/Tween.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | func tween(from: CGFloat, to: CGFloat, progress: CGFloat) -> CGFloat { 5 | return ((to - from) * progress) + from 6 | } 7 | -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-1.imageset/coffee-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/coffee-1.imageset/coffee-1.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-2.imageset/coffee-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/coffee-2.imageset/coffee-2.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-3.imageset/coffee-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/coffee-3.imageset/coffee-3.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-4.imageset/coffee-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/coffee-4.imageset/coffee-4.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-1.imageset/scenic-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/scenic-1.imageset/scenic-1.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-2.imageset/scenic-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/scenic-2.imageset/scenic-2.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-3.imageset/scenic-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/scenic-3.imageset/scenic-3.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-4.imageset/scenic-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/scenic-4.imageset/scenic-4.jpeg -------------------------------------------------------------------------------- /Parchment/Classes/PagingInvalidationContext.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class PagingInvalidationContext: UICollectionViewLayoutInvalidationContext { 4 | var invalidateSizes: Bool = false 5 | } 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-1.imageset/succulents-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/succulents-1.imageset/succulents-1.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-2.imageset/succulents-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/succulents-2.imageset/succulents-2.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-3.imageset/succulents-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/succulents-3.imageset/succulents-3.jpeg -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-4.imageset/succulents-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/Parchment/master/Example/Resources/Assets.xcassets/Unsplash/succulents-4.imageset/succulents-4.jpeg -------------------------------------------------------------------------------- /Parchment.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Parchment/Protocols/PagingMenuDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol PagingMenuDelegate: AnyObject { 4 | func selectContent(pagingItem: PagingItem, direction: PagingDirection, animated: Bool) 5 | func removeContent() 6 | } 7 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingBorderOptions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum PagingBorderOptions { 4 | case hidden 5 | case visible( 6 | height: CGFloat, 7 | zIndex: Int, 8 | insets: UIEdgeInsets 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /Parchment/Extensions/UIEdgeInsets.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIEdgeInsets { 4 | var horizontal: CGFloat { 5 | return left + right 6 | } 7 | 8 | var vertical: CGFloat { 9 | return top + bottom 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Parchment/Protocols/PagingMenuDataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol PagingMenuDataSource: AnyObject { 4 | func pagingItemBefore(pagingItem: PagingItem) -> PagingItem? 5 | func pagingItemAfter(pagingItem: PagingItem) -> PagingItem? 6 | } 7 | -------------------------------------------------------------------------------- /ParchmentTests/Resources.xcassets/Green.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Green.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingIndicatorOptions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum PagingIndicatorOptions { 4 | case hidden 5 | case visible( 6 | height: CGFloat, 7 | zIndex: Int, 8 | spacing: UIEdgeInsets, 9 | insets: UIEdgeInsets 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /Parchment/Protocols/PageViewManagerDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol PageViewManagerDataSource: AnyObject { 4 | func viewControllerBefore(_ viewController: UIViewController) -> UIViewController? 5 | func viewControllerAfter(_ viewController: UIViewController) -> UIViewController? 6 | } 7 | -------------------------------------------------------------------------------- /Parchment.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Parchment/Protocols/PagingLayout.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Used to be able to initialize a layout based on the type defined 4 | /// in the menuLayoutClass property. 5 | protocol PagingLayout { 6 | init() 7 | } 8 | 9 | func createLayout(layout: T.Type) -> T where T: PagingLayout { 10 | return layout.init() 11 | } 12 | -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/axe.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "axe.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "map.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/sun.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sun.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/cloud.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cloud.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/earth.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "earth.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/knife.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "knife.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/leave.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "leave.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/light.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "light.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/moon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "moon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/shoes.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "shoes.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/snow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "snow.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/star.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "star.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/tipi.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tipi.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/tree.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tree.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/water.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "water.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/wind.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "wind.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/wood.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "wood.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | Carthage/Checkouts 21 | Carthage/Build 22 | -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/bonnet.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bonnet.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/compass.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "compass.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Icons/mushroom.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "mushroom.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Parchment/Structs/PagingTransition.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct PagingTransition: Equatable { 4 | let contentOffset: CGPoint 5 | let distance: CGFloat 6 | 7 | static func == (lhs: PagingTransition, rhs: PagingTransition) -> Bool { 8 | return (lhs.contentOffset == rhs.contentOffset && lhs.distance == rhs.distance) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingMenuHorizontalAlignment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PagingMenuHorizontalAlignment { 4 | case left 5 | 6 | // Allows all paging items to be centered within the paging menu 7 | // when PagingMenuItemSize is .fixed and the sum of the widths 8 | // of all the paging items are less than the paging menu 9 | case center 10 | } 11 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/MockPagingControllerSizeDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | 4 | final class MockPagingControllerSizeDelegate: PagingControllerSizeDelegate { 5 | var pagingItemWidth: (() -> CGFloat?)? 6 | 7 | func width(for _: PagingItem, isSelected _: Bool) -> CGFloat { 8 | return pagingItemWidth?() ?? 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingMenuTransition.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PagingMenuTransition { 4 | // Update scroll offset based on how much the content has 5 | // scrolled. Makes the menu items transition smoothly as you scroll. 6 | case scrollAlongside 7 | 8 | // Animate the menu item position after a transition has completed. 9 | case animateAfter 10 | } 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Parchment", 6 | platforms: [.iOS("9.0")], 7 | products: [ 8 | .library(name: "Parchment", targets: ["Parchment"]), 9 | ], 10 | targets: [ 11 | .target( 12 | name: "Parchment", 13 | path: "Parchment" 14 | ), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Header/Header.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Header.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "city-1.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "city-2.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "city-3.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/city-4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "city-4.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "food-1.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "food-2.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "food-3.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/food-4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "food-4.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "coffee-1.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "coffee-2.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "coffee-3.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/coffee-4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "coffee-4.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "green-1.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "green-2.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "green-3.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/green-4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "green-4.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "scenic-1.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "scenic-2.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "scenic-3.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/scenic-4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "scenic-4.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Parchment/Enums/PagingSelectedScrollPosition.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PagingSelectedScrollPosition { 4 | case left 5 | case right 6 | case center 7 | 8 | /// Centers the selected menu item where possible. If the item is 9 | /// to the far left or right, it will not update the scroll 10 | /// position. Effectivly the same as .centeredHorizontally on 11 | /// UICollectionViewScrollPosition. 12 | case preferCentered 13 | } 14 | -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "succulents-1.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "succulents-2.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "succulents-3.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Resources/Assets.xcassets/Unsplash/succulents-4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "succulents-4.jpeg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Parchment/Enums/PageViewState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum PageViewState { 4 | case empty 5 | case single 6 | case first 7 | case center 8 | case last 9 | 10 | var count: Int { 11 | switch self { 12 | case .empty: 13 | return 0 14 | case .single: 15 | return 1 16 | case .first, .last: 17 | return 2 18 | case .center: 19 | return 3 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingDirection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum PagingDirection: Equatable { 5 | case reverse(sibling: Bool) 6 | case forward(sibling: Bool) 7 | case none 8 | } 9 | 10 | extension PagingDirection { 11 | var pageViewControllerNavigationDirection: UIPageViewController.NavigationDirection { 12 | switch self { 13 | case .forward, .none: 14 | return .forward 15 | case .reverse: 16 | return .reverse 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Parchment/Parchment.h: -------------------------------------------------------------------------------- 1 | // 2 | // Parchment.h 3 | // Parchment 4 | // 5 | // Created by Martin on 2016-01-23. 6 | // Copyright © 2016 Martin Rechsteiner. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Parchment. 12 | FOUNDATION_EXPORT double ParchmentVersionNumber; 13 | 14 | //! Project version string for Parchment. 15 | FOUNDATION_EXPORT const unsigned char ParchmentVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /ExampleSwiftUI/DefaultView.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import SwiftUI 3 | import UIKit 4 | 5 | struct DefaultView: View { 6 | let items = [ 7 | PagingIndexItem(index: 0, title: "View 0"), 8 | PagingIndexItem(index: 1, title: "View 1"), 9 | PagingIndexItem(index: 2, title: "View 2"), 10 | PagingIndexItem(index: 3, title: "View 3"), 11 | ] 12 | 13 | var body: some View { 14 | PageView(items: items) { item in 15 | Text(item.title) 16 | .font(.largeTitle) 17 | .foregroundColor(.gray) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Parchment/Structs/AnyPagingItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AnyPagingItem: PagingItem, Hashable, Comparable { 4 | let base: PagingItem 5 | 6 | init(base: PagingItem) { 7 | self.base = base 8 | } 9 | 10 | func hash(into hasher: inout Hasher) { 11 | hasher.combine(base.identifier) 12 | } 13 | 14 | static func < (lhs: AnyPagingItem, rhs: AnyPagingItem) -> Bool { 15 | return lhs.base.isBefore(item: rhs.base) 16 | } 17 | 18 | static func == (lhs: AnyPagingItem, rhs: AnyPagingItem) -> Bool { 19 | return lhs.base.isEqual(to: rhs.base) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingMenuItemSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum PagingMenuItemSource { 5 | case `class`(type: PagingCell.Type) 6 | case nib(nib: UINib) 7 | } 8 | 9 | extension PagingMenuItemSource: Equatable { 10 | public static func == (lhs: PagingMenuItemSource, rhs: PagingMenuItemSource) -> Bool { 11 | switch (lhs, rhs) { 12 | case let (.class(lhsType), .class(rhsType)): 13 | return lhsType == rhsType 14 | 15 | case let (.nib(lhsNib), .nib(rhsNib)): 16 | return lhsNib === rhsNib 17 | 18 | default: 19 | return false 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ParchmentTests/Item.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | 4 | struct Item: PagingItem, Equatable, Comparable { 5 | let index: Int 6 | 7 | var identifier: Int { 8 | return index 9 | } 10 | } 11 | 12 | func == (lhs: Item, rhs: Item) -> Bool { 13 | return lhs.index == rhs.index 14 | } 15 | 16 | func < (lhs: Item, rhs: Item) -> Bool { 17 | return lhs.index < rhs.index 18 | } 19 | 20 | final class ItemCell: PagingCell { 21 | private(set) var item: Item? 22 | 23 | override func setPagingItem(_ pagingItem: PagingItem, selected _: Bool, options _: PagingOptions) { 24 | item = pagingItem as? Item 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/MockPageViewManagerDataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | import XCTest 4 | 5 | final class MockPageViewManagerDataSource: PageViewManagerDataSource { 6 | var viewControllerBefore: ((UIViewController) -> UIViewController?)? 7 | var viewControllerAfter: ((UIViewController) -> UIViewController?)? 8 | 9 | func viewControllerBefore(_ viewController: UIViewController) -> UIViewController? { 10 | viewControllerBefore?(viewController) 11 | } 12 | 13 | func viewControllerAfter(_ viewController: UIViewController) -> UIViewController? { 14 | viewControllerAfter?(viewController) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ExampleSwiftUI/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct ExampleApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | NavigationView { 8 | List { 9 | NavigationLink("Default", destination: DefaultView()) 10 | NavigationLink("Change selected index", destination: SelectedIndexView()) 11 | NavigationLink("Lifecycle events", destination: LifecycleView()) 12 | NavigationLink("Change items", destination: ChangeItemsView()) 13 | } 14 | .navigationBarTitleDisplayMode(.inline) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingBorderView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A custom `UICollectionViewReusableView` subclass used to display 4 | /// the border at the bottom of the menu items. You can subclass this 5 | /// type if you need further customization; just override the 6 | /// `borderClass` property in `PagingViewController`. 7 | open class PagingBorderView: UICollectionReusableView { 8 | open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 9 | super.apply(layoutAttributes) 10 | if let attributes = layoutAttributes as? PagingBorderLayoutAttributes { 11 | backgroundColor = attributes.backgroundColor 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingIndicatorView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A custom `UICollectionViewReusableView` subclass used to display a 4 | /// view that indicates the currently selected cell. You can subclass 5 | /// this type if you need further customization; just override the 6 | /// `indicatorClass` property in `PagingViewController`. 7 | open class PagingIndicatorView: UICollectionReusableView { 8 | open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 9 | super.apply(layoutAttributes) 10 | if let attributes = layoutAttributes as? PagingIndicatorLayoutAttributes { 11 | backgroundColor = attributes.backgroundColor 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build-and-test: 4 | macos: 5 | xcode: "11.3.0" 6 | steps: 7 | - checkout 8 | - run: 9 | name: Run Unit Tests 10 | command: xcodebuild -project Parchment.xcodeproj -scheme "Parchment" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11,OS=13.3' test 11 | - run: 12 | name: Run UI Tests 13 | command: xcodebuild -project Parchment.xcodeproj -scheme "ParchmentUITests" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11,OS=13.3' test 14 | 15 | 16 | workflows: 17 | version: 2 18 | build-and-test: 19 | jobs: 20 | - build-and-test 21 | -------------------------------------------------------------------------------- /Parchment/Enums/PageViewDirection.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | 4 | public enum PageViewDirection { 5 | case forward 6 | case reverse 7 | case none 8 | 9 | init(from direction: PagingDirection) { 10 | switch direction { 11 | case .forward: 12 | self = .forward 13 | case .reverse: 14 | self = .reverse 15 | case .none: 16 | self = .none 17 | } 18 | } 19 | 20 | init(progress: CGFloat) { 21 | if progress > 0 { 22 | self = .forward 23 | } else if progress < 0 { 24 | self = .reverse 25 | } else { 26 | self = .none 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example/Examples/Calendar/DateFormatters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct DateFormatters { 4 | static var shortDateFormatter: DateFormatter = { 5 | let dateFormatter = DateFormatter() 6 | dateFormatter.timeStyle = .none 7 | dateFormatter.dateStyle = .short 8 | return dateFormatter 9 | }() 10 | 11 | static var dateFormatter: DateFormatter = { 12 | let dateFormatter = DateFormatter() 13 | dateFormatter.dateFormat = "d" 14 | return dateFormatter 15 | }() 16 | 17 | static var weekdayFormatter: DateFormatter = { 18 | let dateFormatter = DateFormatter() 19 | dateFormatter.dateFormat = "EEE" 20 | return dateFormatter 21 | }() 22 | } 23 | -------------------------------------------------------------------------------- /ParchmentTests/UIColorInterpolationTests.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | @testable import Parchment 3 | import XCTest 4 | 5 | final class UIColorInterpolationTests: XCTestCase { 6 | // Colors initialized with UIColor(patternImage:) have only 1 7 | // color component. This test ensures we don't crash. 8 | func testImageFromPatternImageDefaultToBlack() { 9 | let from = UIColor.red 10 | let bundle = Bundle(for: Self.self) 11 | let image = UIImage(named: "Green", in: bundle, compatibleWith: nil)! 12 | let to = UIColor(patternImage: image) 13 | let result = UIColor.interpolate(from: from, to: to, with: 1) 14 | 15 | XCTAssertEqual(result, UIColor(red: 0, green: 0, blue: 0, alpha: 1)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ExampleSwiftUI/SelectedIndexView.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import SwiftUI 3 | import UIKit 4 | 5 | struct SelectedIndexView: View { 6 | var items = [ 7 | PagingIndexItem(index: 0, title: "View 0"), 8 | PagingIndexItem(index: 1, title: "View 1"), 9 | PagingIndexItem(index: 2, title: "View 2"), 10 | PagingIndexItem(index: 3, title: "View 3"), 11 | ] 12 | @State var selectedIndex: Int = 2 13 | 14 | var body: some View { 15 | PageView(items: items, selectedIndex: $selectedIndex) { item in 16 | Text(item.title) 17 | .font(.largeTitle) 18 | .foregroundColor(.gray) 19 | .onTapGesture { 20 | selectedIndex = 0 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/Examples/Icons/IconViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class IconViewController: UIViewController { 4 | init(title: String) { 5 | super.init(nibName: nil, bundle: nil) 6 | self.title = title 7 | 8 | let label = UILabel(frame: .zero) 9 | label.font = UIFont.systemFont(ofSize: 50, weight: UIFont.Weight.thin) 10 | label.textColor = UIColor(red: 95 / 255, green: 102 / 255, blue: 108 / 255, alpha: 1) 11 | label.text = title.capitalized 12 | label.sizeToFit() 13 | 14 | view.addSubview(label) 15 | view.constrainCentered(label) 16 | view.backgroundColor = .white 17 | } 18 | 19 | required init?(coder _: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/Resources/TableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class TableViewController: UITableViewController { 4 | private static let CellIdentifier = "CellIdentifier" 5 | 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: TableViewController.CellIdentifier) 9 | } 10 | 11 | override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 12 | return 500 13 | } 14 | 15 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 16 | let cell = tableView.dequeueReusableCell(withIdentifier: TableViewController.CellIdentifier, for: indexPath) 17 | cell.textLabel?.text = "Title \(indexPath.row)" 18 | return cell 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ParchmentUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/MockPagingControllerDataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | 4 | final class MockPagingControllerDataSource: PagingMenuDataSource { 5 | var maxIndexAfter: Int = Int.max 6 | var minIndexBefore: Int = Int.min 7 | 8 | func pagingItemBefore(pagingItem: PagingItem) -> PagingItem? { 9 | guard let item = pagingItem as? Item else { return nil } 10 | if item.index > minIndexBefore { 11 | return Item(index: item.index - 1) 12 | } 13 | return nil 14 | } 15 | 16 | func pagingItemAfter(pagingItem: PagingItem) -> PagingItem? { 17 | guard let item = pagingItem as? Item else { return nil } 18 | if item.index < maxIndexAfter { 19 | return Item(index: item.index + 1) 20 | } 21 | return nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ParchmentTests/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Examples/Images/ImageCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ImageCollectionViewCell: UICollectionViewCell { 4 | static let reuseIdentifier: String = "ImageCellIdentifier" 5 | 6 | fileprivate lazy var imageView: UIImageView = { 7 | let imageView = UIImageView(frame: .zero) 8 | imageView.contentMode = .scaleAspectFill 9 | return imageView 10 | }() 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | contentView.clipsToBounds = true 15 | contentView.addSubview(imageView) 16 | contentView.constrainToEdges(imageView) 17 | } 18 | 19 | required init?(coder _: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | func setImage(_ image: UIImage) { 24 | imageView.image = image 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ExampleSwiftUI/ChangeItems.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import SwiftUI 3 | import UIKit 4 | 5 | struct ChangeItemsView: View { 6 | @State var items = [ 7 | PagingIndexItem(index: 0, title: "View 0"), 8 | PagingIndexItem(index: 1, title: "View 1"), 9 | PagingIndexItem(index: 2, title: "View 2"), 10 | PagingIndexItem(index: 3, title: "View 3"), 11 | ] 12 | 13 | var body: some View { 14 | PageView(items: items) { item in 15 | Text(item.title) 16 | .font(.largeTitle) 17 | .foregroundColor(.gray) 18 | .onTapGesture { 19 | items = [ 20 | PagingIndexItem(index: 0, title: "View 5"), 21 | PagingIndexItem(index: 1, title: "View 6"), 22 | ] 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Parchment/Protocols/PagingViewControllerSizeDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol PagingViewControllerSizeDelegate: AnyObject { 4 | /// Manually control the width for a given `PagingItem`. Parchment 5 | /// does not support self-sizing cells, so you have to use this if 6 | /// you have a cell that you want to size based on its content. 7 | /// 8 | /// - Parameter pagingViewController: The `PagingViewController` 9 | /// instance 10 | /// - Parameter pagingItem: The `PagingItem` instance 11 | /// - Parameter isSelected: A boolean that indicates whether the 12 | /// given `PagingItem` is selected 13 | /// - Returns: The width for the `PagingItem` 14 | func pagingViewController( 15 | _: PagingViewController, 16 | widthForPagingItem pagingItem: PagingItem, 17 | isSelected: Bool 18 | ) -> CGFloat 19 | } 20 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingCellLayoutAttributes.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A custom `UICollectionViewLayoutAttributes` subclass that adds a 4 | /// `progress` property indicating how far the user has scrolled. 5 | open class PagingCellLayoutAttributes: UICollectionViewLayoutAttributes { 6 | open var progress: CGFloat = 0.0 7 | 8 | open override func copy(with zone: NSZone? = nil) -> Any { 9 | let copy = super.copy(with: zone) as! PagingCellLayoutAttributes 10 | copy.progress = progress 11 | return copy 12 | } 13 | 14 | open override func isEqual(_ object: Any?) -> Bool { 15 | if let rhs = object as? PagingCellLayoutAttributes { 16 | if progress != rhs.progress { 17 | return false 18 | } 19 | return super.isEqual(object) 20 | } else { 21 | return false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ExampleSwiftUI/LifecycleView.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import SwiftUI 3 | import UIKit 4 | 5 | struct LifecycleView: View { 6 | let items = [ 7 | PagingIndexItem(index: 0, title: "View 0"), 8 | PagingIndexItem(index: 1, title: "View 1"), 9 | PagingIndexItem(index: 2, title: "View 2"), 10 | PagingIndexItem(index: 3, title: "View 3"), 11 | ] 12 | 13 | var body: some View { 14 | PageView(items: items) { item in 15 | Text(item.title) 16 | .font(.largeTitle) 17 | .foregroundColor(.gray) 18 | } 19 | .willScroll { pagingItem in 20 | print("will scroll: ", pagingItem) 21 | } 22 | .didScroll { pagingItem in 23 | print("did scroll: ", pagingItem) 24 | } 25 | .didSelect { pagingItem in 26 | print("did select: ", pagingItem) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Parchment/Resources/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 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Parchment/Structs/PagingIndexItem.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// An implementation of the `PagingItem` protocol that stores the 4 | /// index and title of a given item. The index property is needed to 5 | /// make the `PagingItem` comparable. 6 | public struct PagingIndexItem: PagingItem, Hashable, Comparable { 7 | /// The index of the `PagingItem` instance 8 | public let index: Int 9 | 10 | /// The title used in the menu cells. 11 | public let title: String 12 | 13 | /// Creates an instance of `PagingIndexItem` 14 | /// 15 | /// Parameter index: The index of the `PagingItem`. 16 | /// Parameter title: The title used in the menu cells. 17 | public init(index: Int, title: String) { 18 | self.index = index 19 | self.title = title 20 | } 21 | 22 | public static func < (lhs: PagingIndexItem, rhs: PagingIndexItem) -> Bool { 23 | return lhs.index < rhs.index 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Parchment/Structs/PagingCellViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | struct PagingTitleCellViewModel { 5 | let title: String? 6 | let font: UIFont 7 | let selectedFont: UIFont 8 | let textColor: UIColor 9 | let selectedTextColor: UIColor 10 | let backgroundColor: UIColor 11 | let selectedBackgroundColor: UIColor 12 | let selected: Bool 13 | let labelSpacing: CGFloat 14 | 15 | init(title: String?, selected: Bool, options: PagingOptions) { 16 | self.title = title 17 | font = options.font 18 | selectedFont = options.selectedFont 19 | textColor = options.textColor 20 | selectedTextColor = options.selectedTextColor 21 | backgroundColor = options.backgroundColor 22 | selectedBackgroundColor = options.selectedBackgroundColor 23 | self.selected = selected 24 | labelSpacing = options.menuItemLabelSpacing 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/MockPagingControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | 4 | final class MockPagingControllerDelegate: PagingMenuDelegate, Mock { 5 | enum Action: Equatable { 6 | case selectContent(pagingItem: Item, direction: PagingDirection, animated: Bool) 7 | case removeContent 8 | } 9 | 10 | var calls: [MockCall] = [] 11 | 12 | func selectContent(pagingItem: PagingItem, direction: PagingDirection, animated: Bool) { 13 | calls.append(MockCall( 14 | datetime: Date(), 15 | action: .delegate(.selectContent( 16 | pagingItem: pagingItem as! Item, 17 | direction: direction, 18 | animated: animated 19 | )) 20 | )) 21 | } 22 | 23 | func removeContent() { 24 | calls.append(MockCall( 25 | datetime: Date(), 26 | action: .delegate(.removeContent) 27 | )) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Parchment.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Parchment", 3 | "version": "3.0.1", 4 | "license": "MIT", 5 | "summary": "A flexible paging menu controller with support for infinite data sources.", 6 | "description": "Parchment allows you to page between view controllers while showing menu items that scrolls along with the content. It’s build to be very customizable, it’s well-tested and written fully in Swift.", 7 | "homepage": "https://github.com/rechsteiner/Parchment", 8 | "authors": { 9 | "Martin Rechsteiner": "marrechsteiner@gmail.com" 10 | }, 11 | "social_media_url": "http://twitter.com/rechsteiner", 12 | "source": { 13 | "git": "https://github.com/rechsteiner/Parchment.git", 14 | "tag": "v3.0.1" 15 | }, 16 | "platforms": { 17 | "ios": "8.2" 18 | }, 19 | "weak_frameworks": [ 20 | "SwiftUI", 21 | "Combine" 22 | ], 23 | "source_files": [ 24 | "Parchment/**/*.swift", 25 | "Parchment/*.swift" 26 | ], 27 | "requires_arc": true 28 | } 29 | -------------------------------------------------------------------------------- /Example/Resources/UIColor+interpolation.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | // Extension to interpolate between two UIColor values. 4 | // Based on http://stackoverflow.com/a/35853850 5 | 6 | extension UIColor { 7 | func components() -> (CGFloat, CGFloat, CGFloat, CGFloat) { 8 | guard let c = cgColor.components else { return (0, 0, 0, 1) } 9 | if cgColor.numberOfComponents == 2 { 10 | return (c[0], c[0], c[0], c[1]) 11 | } else { 12 | return (c[0], c[1], c[2], c[3]) 13 | } 14 | } 15 | 16 | static func interpolate(from: UIColor, to: UIColor, with fraction: CGFloat) -> UIColor { 17 | let f = min(1, max(0, fraction)) 18 | let c1 = from.components() 19 | let c2 = to.components() 20 | let r = c1.0 + (c2.0 - c1.0) * f 21 | let g = c1.1 + (c2.1 - c1.1) * f 22 | let b = c1.2 + (c2.2 - c1.2) * f 23 | let a = c1.3 + (c2.3 - c1.3) * f 24 | return UIColor(red: r, green: g, blue: b, alpha: a) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ParchmentTests/PagingViewTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Parchment 2 | import XCTest 3 | 4 | final class PagingViewTests: XCTestCase { 5 | var pagingView: PagingView! 6 | var collectionView: UICollectionView! 7 | 8 | override func setUp() { 9 | let options = PagingOptions() 10 | let pageView = UIView(frame: .zero) 11 | 12 | collectionView = UICollectionView( 13 | frame: .zero, 14 | collectionViewLayout: UICollectionViewLayout() 15 | ) 16 | 17 | pagingView = PagingView( 18 | options: options, 19 | collectionView: collectionView, 20 | pageView: pageView 21 | ) 22 | } 23 | 24 | func testMenuBackgroundColor() { 25 | pagingView.configure() 26 | 27 | XCTAssertEqual(collectionView.backgroundColor, .white) 28 | 29 | var options = PagingOptions() 30 | options.menuBackgroundColor = .green 31 | pagingView.options = options 32 | 33 | XCTAssertEqual(collectionView.backgroundColor, .green) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/Resources/ContentViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ContentViewController: UIViewController { 4 | convenience init(index: Int) { 5 | self.init(title: "View \(index)", content: "\(index)") 6 | } 7 | 8 | convenience init(title: String) { 9 | self.init(title: title, content: title) 10 | } 11 | 12 | init(title: String, content: String) { 13 | super.init(nibName: nil, bundle: nil) 14 | self.title = title 15 | 16 | let label = UILabel(frame: .zero) 17 | label.font = UIFont.systemFont(ofSize: 50, weight: UIFont.Weight.thin) 18 | label.textColor = UIColor(red: 95 / 255, green: 102 / 255, blue: 108 / 255, alpha: 1) 19 | label.textAlignment = .center 20 | label.text = content 21 | label.sizeToFit() 22 | 23 | view.addSubview(label) 24 | view.constrainToEdges(label) 25 | view.backgroundColor = .white 26 | } 27 | 28 | required init?(coder _: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Parchment/Protocols/PagingItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The `PagingItem` protocol is used to generate menu items for all 4 | /// the view controllers, without having to actually allocate them 5 | /// before they are needed. You can store whatever you want in here 6 | /// that makes sense for what you're displaying. 7 | public protocol PagingItem { 8 | var identifier: Int { get } 9 | func isEqual(to item: PagingItem) -> Bool 10 | func isBefore(item: PagingItem) -> Bool 11 | } 12 | 13 | extension PagingItem where Self: Equatable { 14 | public func isEqual(to item: PagingItem) -> Bool { 15 | guard let item = item as? Self else { return false } 16 | return self == item 17 | } 18 | } 19 | 20 | extension PagingItem where Self: Comparable { 21 | public func isBefore(item: PagingItem) -> Bool { 22 | guard let item = item as? Self else { return false } 23 | return self < item 24 | } 25 | } 26 | 27 | extension PagingItem where Self: Hashable { 28 | public var identifier: Int { 29 | return hashValue 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A custom `UICollectionViewCell` subclass used to display the menu 4 | /// items. When creating your own custom cells, you need to subclass 5 | /// this type instead of `UICollectionViewCell` directly. 6 | open class PagingCell: UICollectionViewCell { 7 | /// Called by the `PagingViewControllerDataSource` to customize the 8 | /// cell with an instance conforming to `PagingItem`. You have to 9 | /// override this method when creating your own subclass – the 10 | /// default implementation will crash. 11 | /// 12 | /// - Parameter pagingItem: The `PagingItem` that is provided by the 13 | /// data source. 14 | /// - Parameter selected: A boolean to indicate whether the cell is 15 | /// currently selected. 16 | /// - Parameter options: The `PagingOptions` used to customize the 17 | /// look and feel of the `PagingViewController. 18 | open func setPagingItem(_: PagingItem, selected _: Bool, options _: PagingOptions) { 19 | fatalError("setPagingItem: not implemented") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Parchment/Extensions/UIColor+interpolation.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | // Extension to interpolate between two UIColor values. 4 | // Based on http://stackoverflow.com/a/35853850 5 | 6 | extension UIColor { 7 | func components() -> (CGFloat, CGFloat, CGFloat, CGFloat) { 8 | guard let c = cgColor.components else { return (0, 0, 0, 1) } 9 | if cgColor.numberOfComponents == 1 { 10 | return (0, 0, 0, 1) 11 | } else if cgColor.numberOfComponents == 2 { 12 | return (c[0], c[0], c[0], c[1]) 13 | } else { 14 | return (c[0], c[1], c[2], c[3]) 15 | } 16 | } 17 | 18 | static func interpolate(from: UIColor, to: UIColor, with fraction: CGFloat) -> UIColor { 19 | let f = min(1, max(0, fraction)) 20 | let c1 = from.components() 21 | let c2 = to.components() 22 | let r = c1.0 + (c2.0 - c1.0) * f 23 | let g = c1.1 + (c2.1 - c1.1) * f 24 | let b = c1.2 + (c2.2 - c1.2) * f 25 | let a = c1.3 + (c2.3 - c1.3) * f 26 | return UIColor(red: r, green: g, blue: b, alpha: a) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Parchment/Protocols/PageViewControllerDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// The `PageViewControllerDataSource` protocol is used to provide 4 | /// the view controller you want to display. 5 | public protocol PageViewControllerDataSource: AnyObject { 6 | /// Return the view controller before a given view controller. 7 | /// 8 | /// - Parameters: 9 | /// - pageViewController: The `PageViewController` instance. 10 | /// - viewController: The current view controller. 11 | func pageViewController( 12 | _ pageViewController: PageViewController, 13 | viewControllerBeforeViewController viewController: UIViewController 14 | ) -> UIViewController? 15 | 16 | /// Return the view controller after a given view controller. 17 | /// 18 | /// - Parameters: 19 | /// - pageViewController: The `PageViewController` instance. 20 | /// - viewController: The current view controller. 21 | func pageViewController( 22 | _ pageViewController: PageViewController, 23 | viewControllerAfterViewController viewController: UIViewController 24 | ) -> UIViewController? 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Martin Rechsteiner 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 | -------------------------------------------------------------------------------- /Parchment/Protocols/PageViewManagerDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol PageViewManagerDelegate: AnyObject { 4 | func scrollForward() 5 | func scrollReverse() 6 | func layoutViews(for viewControllers: [UIViewController], keepContentOffset: Bool) 7 | func addViewController(_ viewController: UIViewController) 8 | func removeViewController(_ viewController: UIViewController) 9 | func beginAppearanceTransition( 10 | isAppearing: Bool, 11 | viewController: UIViewController, 12 | animated: Bool 13 | ) 14 | func endAppearanceTransition(viewController: UIViewController) 15 | func willScroll( 16 | from selectedViewController: UIViewController, 17 | to destinationViewController: UIViewController 18 | ) 19 | func isScrolling( 20 | from selectedViewController: UIViewController, 21 | to destinationViewController: UIViewController?, 22 | progress: CGFloat 23 | ) 24 | func didFinishScrolling( 25 | from selectedViewController: UIViewController, 26 | to destinationViewController: UIViewController, 27 | transitionSuccessful: Bool 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/Mock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Action: Equatable { 4 | case collectionView(MockCollectionView.Action) 5 | case collectionViewLayout(MockCollectionViewLayout.Action) 6 | case delegate(MockPagingControllerDelegate.Action) 7 | } 8 | 9 | struct MockCall { 10 | let datetime: Date 11 | let action: Action 12 | } 13 | 14 | extension MockCall: Equatable { 15 | static func == (lhs: MockCall, rhs: MockCall) -> Bool { 16 | return lhs.datetime == rhs.datetime && lhs.action == rhs.action 17 | } 18 | } 19 | 20 | extension MockCall: Comparable { 21 | static func < (lhs: MockCall, rhs: MockCall) -> Bool { 22 | return lhs.datetime < rhs.datetime 23 | } 24 | } 25 | 26 | protocol Mock { 27 | var calls: [MockCall] { get } 28 | } 29 | 30 | func actions(_ calls: [MockCall]) -> [Action] { 31 | return calls.map { $0.action } 32 | } 33 | 34 | func combinedActions(_ a: [MockCall], _ b: [MockCall]) -> [Action] { 35 | return actions(Array(a + b).sorted()) 36 | } 37 | 38 | func combinedActions(_ a: [MockCall], _ b: [MockCall], _ c: [MockCall]) -> [Action] { 39 | return actions(Array(a + b + c).sorted()) 40 | } 41 | -------------------------------------------------------------------------------- /Parchment/Structs/PagingIndicatorMetric.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | struct PagingIndicatorMetric { 5 | enum Inset { 6 | case left(CGFloat) 7 | case right(CGFloat) 8 | case both(CGFloat, CGFloat) 9 | case none 10 | } 11 | 12 | let frame: CGRect 13 | let insets: Inset 14 | let spacing: UIEdgeInsets 15 | 16 | var x: CGFloat { 17 | switch insets { 18 | case let .left(inset), let .both(inset, _): 19 | return frame.origin.x + max(inset, spacing.left) 20 | default: 21 | return frame.origin.x + spacing.left 22 | } 23 | } 24 | 25 | var width: CGFloat { 26 | switch insets { 27 | case let .left(inset): 28 | return frame.size.width - max(inset, spacing.left) - spacing.right 29 | case let .right(inset): 30 | return frame.size.width - max(inset, spacing.right) - spacing.left 31 | case let .both(insetLeft, insetRight): 32 | return frame.size.width - max(insetRight, spacing.right) - max(insetLeft, spacing.left) 33 | case .none: 34 | return frame.size.width - spacing.left - spacing.right 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ParchmentUITests/ParchmentUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ParchmentUITests: XCTestCase { 4 | var app: XCUIApplication! 5 | 6 | override func setUp() { 7 | continueAfterFailure = false 8 | app = XCUIApplication() 9 | app.launchArguments = ["--ui-testing"] 10 | app.launch() 11 | } 12 | 13 | func testSelect() { 14 | let cell0 = app.collectionViews.cells["View 0"] 15 | let cell1 = app.collectionViews.cells["View 1"] 16 | 17 | cell1.tap() 18 | let content1 = app.scrollViews.firstMatch.staticTexts["1"] 19 | XCTAssertTrue(content1.waitForExistence(timeout: 1)) 20 | XCTAssertTrue(cell1.isSelected) 21 | 22 | cell0.tap() 23 | let content0 = app.scrollViews.firstMatch.staticTexts["0"] 24 | XCTAssertTrue(content0.waitForExistence(timeout: 1)) 25 | XCTAssertTrue(cell0.isSelected) 26 | } 27 | 28 | func testSwipe() { 29 | app.scrollViews.firstMatch.swipeLeft() 30 | let content1 = app.scrollViews.firstMatch.staticTexts["1"] 31 | XCTAssertTrue(content1.waitForExistence(timeout: 1)) 32 | 33 | let cell1 = app.collectionViews.cells["View 1"] 34 | XCTAssertTrue(cell1.isSelected) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/Examples/Storyboard/StoryboardViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | class StoryboardViewController: UIViewController { 5 | override func viewDidLoad() { 6 | super.viewDidLoad() 7 | 8 | // Load each of the view controllers you want to embed 9 | // from the storyboard. 10 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 11 | let firstViewController = storyboard.instantiateViewController(withIdentifier: "FirstViewController") 12 | let secondViewController = storyboard.instantiateViewController(withIdentifier: "SecondViewController") 13 | 14 | // Initialize a FixedPagingViewController and pass 15 | // in the view controllers. 16 | let pagingViewController = PagingViewController(viewControllers: [ 17 | firstViewController, 18 | secondViewController, 19 | ]) 20 | 21 | // Make sure you add the PagingViewController as a child view 22 | // controller and contrain it to the edges of the view. 23 | addChild(pagingViewController) 24 | view.addSubview(pagingViewController.view) 25 | view.constrainToEdges(pagingViewController.view) 26 | pagingViewController.didMove(toParent: self) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingMenuItemSize.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum PagingMenuItemSize { 5 | case fixed(width: CGFloat, height: CGFloat) 6 | 7 | // Automatically calculate the size of the menu items based on the 8 | // cells intrinsic content size. Try to come up with an estimated 9 | // width that's similar to the expected width of the cells. 10 | case selfSizing(estimatedWidth: CGFloat, height: CGFloat) 11 | 12 | // Tries to fit all menu items inside the bounds of the screen. 13 | // If the items can't fit, the items will scroll as normal and 14 | // set the menu items width to `minWidth`. 15 | case sizeToFit(minWidth: CGFloat, height: CGFloat) 16 | } 17 | 18 | public extension PagingMenuItemSize { 19 | var width: CGFloat { 20 | switch self { 21 | case let .fixed(width, _): return width 22 | case let .sizeToFit(minWidth, _): return minWidth 23 | case let .selfSizing(estimatedWidth, _): return estimatedWidth 24 | } 25 | } 26 | 27 | var height: CGFloat { 28 | switch self { 29 | case let .fixed(_, height): return height 30 | case let .sizeToFit(_, height): return height 31 | case let .selfSizing(_, height): return height 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/Examples/SelfSizing/SelfSizingViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | final class SelfSizingViewController: PagingViewController { 5 | private let movies = [ 6 | "Pulp Fiction", 7 | "The Shawshank Redemption", 8 | "The Dark Knight", 9 | "Fight Club", 10 | "Se7en", 11 | "Saving Private Ryan", 12 | "Interstellar", 13 | "Harakiri", 14 | "Psycho", 15 | "The Intouchables", 16 | "Once Upon a Time in the West", 17 | "Alien", 18 | ] 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | dataSource = self 23 | menuItemSize = .selfSizing(estimatedWidth: 100, height: 40) 24 | } 25 | } 26 | 27 | extension SelfSizingViewController: PagingViewControllerDataSource { 28 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { 29 | return PagingIndexItem(index: index, title: movies[index]) 30 | } 31 | 32 | func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController { 33 | return ContentViewController(title: movies[index]) 34 | } 35 | 36 | func numberOfViewControllers(in _: PagingViewController) -> Int { 37 | return movies.count 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Example/Examples/Basic/BasicViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | // This is the simplest use case of using Parchment. We just create a 5 | // bunch of view controllers, and pass them into our paging view 6 | // controller. FixedPagingViewController is a subclass of 7 | // PagingViewController that makes it much easier to get started with 8 | // Parchment when you only have a fixed array of view controllers. It 9 | // will create a data source for us and set up the paging items to 10 | // display the view controllers title. 11 | class BasicViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | let viewControllers = [ 16 | ContentViewController(index: 0), 17 | ContentViewController(index: 1), 18 | ContentViewController(index: 2), 19 | ContentViewController(index: 3), 20 | ] 21 | 22 | let pagingViewController = PagingViewController(viewControllers: viewControllers) 23 | 24 | // Make sure you add the PagingViewController as a child view 25 | // controller and constrain it to the edges of the view. 26 | addChild(pagingViewController) 27 | view.addSubview(pagingViewController.view) 28 | view.constrainToEdges(pagingViewController.view) 29 | pagingViewController.didMove(toParent: self) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/MockCollectionViewLayout.swift: -------------------------------------------------------------------------------- 1 | @testable import Parchment 2 | import UIKit 3 | 4 | final class MockCollectionViewLayout: CollectionViewLayout, Mock { 5 | enum Action: Equatable { 6 | case prepare 7 | case invalidateLayout 8 | case invalidateLayoutWithContext(invalidateSizes: Bool) 9 | } 10 | 11 | var calls: [MockCall] = [] 12 | var contentInsets: UIEdgeInsets = .zero 13 | var layoutAttributes: [IndexPath: PagingCellLayoutAttributes] = [:] 14 | var state: PagingState = .empty 15 | var visibleItems = PagingItems(items: []) 16 | var sizeCache: PagingSizeCache? 17 | 18 | func prepare() { 19 | calls.append(MockCall( 20 | datetime: Date(), 21 | action: .collectionViewLayout(.prepare) 22 | )) 23 | } 24 | 25 | func invalidateLayout() { 26 | calls.append(MockCall( 27 | datetime: Date(), 28 | action: .collectionViewLayout(.invalidateLayout) 29 | )) 30 | } 31 | 32 | func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { 33 | let context = context as! PagingInvalidationContext 34 | calls.append(MockCall( 35 | datetime: Date(), 36 | action: .collectionViewLayout(.invalidateLayoutWithContext( 37 | invalidateSizes: context.invalidateSizes 38 | )) 39 | )) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Documentation/basic-usage.md: -------------------------------------------------------------------------------- 1 | # Basic usage 2 | 3 | The easiest way of using Parchment is to initialize `PagingViewController` with the an array of the view controllers you want to display: 4 | 5 | ```Swift 6 | import Parchment 7 | 8 | class ViewController: UIViewController { 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | let firstViewController = UIViewController() 12 | let secondViewController = UIViewController() 13 | 14 | let pagingViewController = PagingViewController(viewControllers: [ 15 | firstViewController, 16 | secondViewController 17 | ]) 18 | } 19 | } 20 | ``` 21 | 22 | Then add the `pagingViewController` as a child view controller and setup the constraints for the view: 23 | 24 | ```Swift 25 | addChild(pagingViewController) 26 | view.addSubview(pagingViewController.view) 27 | pagingViewController.didMove(toParent: self) 28 | pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false 29 | 30 | NSLayoutConstraint.activate([ 31 | pagingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 32 | pagingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 33 | pagingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 34 | pagingViewController.view.topAnchor.constraint(equalTo: view.topAnchor) 35 | ]) 36 | ``` 37 | 38 | Parchment will then generate menu items for each view controller using their title property. 39 | 40 | _Check out the Example target for more details._ 41 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingFiniteDataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class PagingFiniteDataSource: PagingViewControllerInfiniteDataSource { 5 | var items: [PagingItem] = [] 6 | var viewControllerForIndex: ((Int) -> UIViewController?)? 7 | 8 | func pagingViewController(_: PagingViewController, viewControllerFor pagingItem: PagingItem) -> UIViewController { 9 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { 10 | fatalError("pagingViewController:viewControllerFor: PagingItem does not exist") 11 | } 12 | guard let viewController = viewControllerForIndex?(index) else { 13 | fatalError("pagingViewController:viewControllerFor: No view controller exist for PagingItem") 14 | } 15 | 16 | return viewController 17 | } 18 | 19 | func pagingViewController(_: PagingViewController, itemBefore pagingItem: PagingItem) -> PagingItem? { 20 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { return nil } 21 | if index > 0 { 22 | return items[index - 1] 23 | } 24 | return nil 25 | } 26 | 27 | func pagingViewController(_: PagingViewController, itemAfter pagingItem: PagingItem) -> PagingItem? { 28 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { return nil } 29 | if index < items.count - 1 { 30 | return items[index + 1] 31 | } 32 | return nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Parchment/Extensions/UIView+constraints.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | func constrainToEdges(_ subview: UIView) { 5 | subview.translatesAutoresizingMaskIntoConstraints = false 6 | 7 | let topContraint = NSLayoutConstraint( 8 | item: subview, 9 | attribute: .top, 10 | relatedBy: .equal, 11 | toItem: self, 12 | attribute: .top, 13 | multiplier: 1.0, 14 | constant: 0 15 | ) 16 | 17 | let bottomConstraint = NSLayoutConstraint( 18 | item: subview, 19 | attribute: .bottom, 20 | relatedBy: .equal, 21 | toItem: self, 22 | attribute: .bottom, 23 | multiplier: 1.0, 24 | constant: 0 25 | ) 26 | 27 | let leadingContraint = NSLayoutConstraint( 28 | item: subview, 29 | attribute: .leading, 30 | relatedBy: .equal, 31 | toItem: self, 32 | attribute: .leading, 33 | multiplier: 1.0, 34 | constant: 0 35 | ) 36 | 37 | let trailingContraint = NSLayoutConstraint( 38 | item: subview, 39 | attribute: .trailing, 40 | relatedBy: .equal, 41 | toItem: self, 42 | attribute: .trailing, 43 | multiplier: 1.0, 44 | constant: 0 45 | ) 46 | 47 | addConstraints([ 48 | topContraint, 49 | bottomConstraint, 50 | leadingContraint, 51 | trailingContraint, 52 | ]) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ExampleSwiftUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Parchment/Enums/InvalidationState.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Used to represent what to invalidate in a collection view 4 | /// layout. We need to be able to invalidate the layout multiple times 5 | /// with different invalidation contexts before `invalidateLayout` is 6 | /// called and we can use we can use this to determine exactly how 7 | /// much we need to invalidate by adding together the states each 8 | /// time a new context is invalidated. 9 | public enum InvalidationState { 10 | case nothing 11 | case everything 12 | case sizes 13 | 14 | public init(_ invalidationContext: UICollectionViewLayoutInvalidationContext) { 15 | if invalidationContext.invalidateEverything { 16 | self = .everything 17 | } else if invalidationContext.invalidateDataSourceCounts { 18 | self = .everything 19 | } else if let context = invalidationContext as? PagingInvalidationContext { 20 | if context.invalidateSizes { 21 | self = .sizes 22 | } else { 23 | self = .nothing 24 | } 25 | } else { 26 | self = .nothing 27 | } 28 | } 29 | 30 | public static func + (lhs: InvalidationState, rhs: InvalidationState) -> InvalidationState { 31 | switch (lhs, rhs) { 32 | case (.everything, _), (_, .everything): 33 | return .everything 34 | case (.sizes, _), (_, .sizes): 35 | return .sizes 36 | case (.nothing, _), (_, .nothing): 37 | return .nothing 38 | default: 39 | return .everything 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/Resources/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ParchmentTests/PagingDataStructureTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | import XCTest 4 | 5 | final class PagingDataTests: XCTestCase { 6 | var visibleItems: PagingItems! 7 | 8 | override func setUp() { 9 | visibleItems = PagingItems(items: [ 10 | Item(index: 0), 11 | Item(index: 1), 12 | Item(index: 2), 13 | ]) 14 | } 15 | 16 | func testIndexPathForPagingItemFound() { 17 | let indexPath = visibleItems.indexPath(for: Item(index: 0))! 18 | XCTAssertEqual(indexPath.item, 0) 19 | } 20 | 21 | func testIndexPathForPagingItemMissing() { 22 | let indexPath = visibleItems.indexPath(for: Item(index: -1)) 23 | XCTAssertNil(indexPath) 24 | } 25 | 26 | func testPagingItemForIndexPath() { 27 | let indexPath = IndexPath(item: 0, section: 0) 28 | let pagingItem = visibleItems.pagingItem(for: indexPath) as! Item 29 | XCTAssertEqual(pagingItem, Item(index: 0)) 30 | } 31 | 32 | func testDirectionForIndexPathForward() { 33 | let currentPagingItem = Item(index: 0) 34 | let upcomingPagingItem = Item(index: 1) 35 | let direction = visibleItems.direction(from: currentPagingItem, to: upcomingPagingItem) 36 | XCTAssertEqual(direction, PagingDirection.forward(sibling: true)) 37 | } 38 | 39 | func testDirectionForIndexPathReverse() { 40 | let currentPagingItem = Item(index: 1) 41 | let upcomingPagingItem = Item(index: 0) 42 | let direction = visibleItems.direction(from: currentPagingItem, to: upcomingPagingItem) 43 | XCTAssertEqual(direction, PagingDirection.reverse(sibling: true)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Parchment/Protocols/PagingViewControllerDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// The `PagingViewControllerDataSource` protocol is used to provide 4 | /// the `PagingItem` you want to display and which view controller it 5 | /// is associated with. Using this data sources requires you to have a 6 | /// fixed number of view controllers. 7 | /// 8 | /// In order for these methods to be called, you first need to set the 9 | /// initial `PagingItem` by calling `select(pagingItem:)` on 10 | /// `PagingViewController`. 11 | public protocol PagingViewControllerDataSource: AnyObject { 12 | /// Return the total number of view controllers 13 | /// 14 | /// - Parameter pagingViewController: The `PagingViewController` 15 | /// instance 16 | /// - Returns: The number of view controllers 17 | func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int 18 | 19 | /// Return the view controller accociated with a given index. This 20 | /// method is only called for the currently selected `PagingItem`, 21 | /// and its two possible siblings. 22 | /// 23 | /// - Parameter pagingViewController: The `PagingViewController` 24 | /// instance 25 | /// - Parameter index: The index of a given `PagingItem` 26 | /// - Returns: The view controller for the given index 27 | func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController 28 | 29 | /// Return the `PagingItem` instance for a given index 30 | /// 31 | /// - Parameter pagingViewController: The `PagingViewController` 32 | /// instance 33 | /// - Returns: The `PagingItem` instance 34 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem 35 | } 36 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingStaticDataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class PagingStaticDataSource: PagingViewControllerInfiniteDataSource { 5 | private(set) var items: [PagingItem] = [] 6 | private let viewControllers: [UIViewController] 7 | 8 | init(viewControllers: [UIViewController]) { 9 | self.viewControllers = viewControllers 10 | reloadItems() 11 | } 12 | 13 | func pagingItem(at index: Int) -> PagingItem { 14 | return items[index] 15 | } 16 | 17 | func reloadItems() { 18 | items = viewControllers.enumerated().map { 19 | PagingIndexItem(index: $0, title: $1.title ?? "") 20 | } 21 | } 22 | 23 | func pagingViewController(_: PagingViewController, viewControllerFor pagingItem: PagingItem) -> UIViewController { 24 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { 25 | fatalError("pagingViewController:viewControllerFor: PagingItem does not exist") 26 | } 27 | return viewControllers[index] 28 | } 29 | 30 | func pagingViewController(_: PagingViewController, itemBefore pagingItem: PagingItem) -> PagingItem? { 31 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { return nil } 32 | if index > 0 { 33 | return items[index - 1] 34 | } 35 | return nil 36 | } 37 | 38 | func pagingViewController(_: PagingViewController, itemAfter pagingItem: PagingItem) -> PagingItem? { 39 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { return nil } 40 | if index < items.count - 1 { 41 | return items[index + 1] 42 | } 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ExampleSwiftUI/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 | -------------------------------------------------------------------------------- /Example/Resources/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 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingIndicatorLayoutAttributes.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class PagingIndicatorLayoutAttributes: UICollectionViewLayoutAttributes { 4 | open var backgroundColor: UIColor? 5 | 6 | open override func copy(with zone: NSZone? = nil) -> Any { 7 | let copy = super.copy(with: zone) as! PagingIndicatorLayoutAttributes 8 | copy.backgroundColor = backgroundColor 9 | return copy 10 | } 11 | 12 | open override func isEqual(_ object: Any?) -> Bool { 13 | if let rhs = object as? PagingIndicatorLayoutAttributes { 14 | if backgroundColor != rhs.backgroundColor { 15 | return false 16 | } 17 | return super.isEqual(object) 18 | } else { 19 | return false 20 | } 21 | } 22 | 23 | func configure(_ options: PagingOptions) { 24 | if case let .visible(height, index, _, insets) = options.indicatorOptions { 25 | backgroundColor = options.indicatorColor 26 | frame.size.height = height 27 | 28 | switch options.menuPosition { 29 | case .top: 30 | frame.origin.y = options.menuHeight - height - insets.bottom + insets.top 31 | case .bottom: 32 | frame.origin.y = insets.bottom 33 | } 34 | zIndex = index 35 | } 36 | } 37 | 38 | func update(from: PagingIndicatorMetric, to: PagingIndicatorMetric, progress: CGFloat) { 39 | frame.origin.x = tween(from: from.x, to: to.x, progress: progress) 40 | frame.size.width = tween(from: from.width, to: to.width, progress: progress) 41 | } 42 | 43 | func update(to metric: PagingIndicatorMetric) { 44 | frame.origin.x = metric.x 45 | frame.size.width = metric.width 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingBorderLayoutAttributes.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class PagingBorderLayoutAttributes: UICollectionViewLayoutAttributes { 4 | open var backgroundColor: UIColor? 5 | open var insets: UIEdgeInsets = UIEdgeInsets() 6 | 7 | open override func copy(with zone: NSZone? = nil) -> Any { 8 | let copy = super.copy(with: zone) as! PagingBorderLayoutAttributes 9 | copy.backgroundColor = backgroundColor 10 | copy.insets = insets 11 | return copy 12 | } 13 | 14 | open override func isEqual(_ object: Any?) -> Bool { 15 | if let rhs = object as? PagingBorderLayoutAttributes { 16 | if backgroundColor != rhs.backgroundColor || insets != rhs.insets { 17 | return false 18 | } 19 | return super.isEqual(object) 20 | } else { 21 | return false 22 | } 23 | } 24 | 25 | func configure(_ options: PagingOptions, safeAreaInsets _: UIEdgeInsets = .zero) { 26 | if case let .visible(height, index, borderInsets) = options.borderOptions { 27 | insets = borderInsets 28 | backgroundColor = options.borderColor 29 | 30 | switch options.menuPosition { 31 | case .top: 32 | frame.origin.y = options.menuHeight - height 33 | case .bottom: 34 | frame.origin.y = 0 35 | } 36 | 37 | frame.size.height = height 38 | zIndex = index 39 | } 40 | } 41 | 42 | func update(contentSize: CGSize, bounds: CGRect, safeAreaInsets: UIEdgeInsets) { 43 | let width = max(bounds.width, contentSize.width) 44 | frame.size.width = width - insets.horizontal - safeAreaInsets.horizontal 45 | frame.origin.x = insets.left + safeAreaInsets.left 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/Examples/MultipleCells/MultipleCellsViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | class MultipleCellsViewController: UIViewController { 5 | let items: [PagingItem] = [ 6 | IconItem(icon: "earth", index: 0), 7 | PagingIndexItem(index: 1, title: "TODO"), 8 | PagingIndexItem(index: 2, title: "In Progress"), 9 | PagingIndexItem(index: 3, title: "Archive"), 10 | PagingIndexItem(index: 4, title: "Other"), 11 | ] 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | let pagingViewController = PagingViewController() 17 | pagingViewController.register(IconPagingCell.self, for: IconItem.self) 18 | pagingViewController.register(PagingTitleCell.self, for: PagingIndexItem.self) 19 | pagingViewController.menuItemSize = .selfSizing(estimatedWidth: 100, height: 60) 20 | pagingViewController.dataSource = self 21 | pagingViewController.select(index: 0) 22 | 23 | // Add the paging view controller as a child view controller 24 | // and contrain it to all edges. 25 | addChild(pagingViewController) 26 | view.addSubview(pagingViewController.view) 27 | 28 | pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false 29 | view.constrainToEdges(pagingViewController.view) 30 | pagingViewController.didMove(toParent: self) 31 | } 32 | } 33 | 34 | extension MultipleCellsViewController: PagingViewControllerDataSource { 35 | func pagingViewController(_: PagingViewController, viewControllerAt _: Int) -> UIViewController { 36 | return TableViewController() 37 | } 38 | 39 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { 40 | return items[index] 41 | } 42 | 43 | func numberOfViewControllers(in _: PagingViewController) -> Int { 44 | return items.count 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/Resources/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 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ExampleSwiftUI/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 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Documentation/data-source.md: -------------------------------------------------------------------------------- 1 | # Using the data source 2 | 3 | Let’s start by defining an array that contains the information we need to display our menu items: 4 | 5 | ```Swift 6 | class ViewController: UIViewController { 7 | let cities = [ 8 | "Oslo", 9 | "Stockholm", 10 | "Tokyo", 11 | "Barcelona", 12 | "Vancouver", 13 | "Berlin" 14 | ] 15 | } 16 | ``` 17 | 18 | Then we initialize our `PagingViewController`: 19 | 20 | ```Swift 21 | class ViewController: UIViewController { 22 | ... 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | let pagingViewController = PagingViewController() 27 | pagingViewController.dataSource = self 28 | } 29 | } 30 | ``` 31 | 32 | In our data source implementation we set the number of view controllers equal to the number of items in our cities array, and return an instance of `PagingTitleItem` with the title of each city: 33 | 34 | ```Swift 35 | extension ViewController: PagingViewControllerDataSource { 36 | func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int { 37 | return cities.count 38 | } 39 | 40 | func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController { 41 | return CityViewController(city: cities[index]) 42 | } 43 | 44 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { 45 | return PagingIndexItem(index: index, title: cities[index]) 46 | } 47 | } 48 | ``` 49 | 50 | The `viewControllerForIndex` method will only be called for the currently selected item and any of its siblings. This means that we only allocate the view controllers that are actually needed at any given point. 51 | 52 | Parchment will automatically set the first item as selected, but if you want to select another you can do it like this: 53 | 54 | ```Swift 55 | pagingViewController.select(index: 3) 56 | ``` 57 | 58 | This can be called both before and after the view has appeared. 59 | 60 | _Check out the DelegateExample target for more details._ -------------------------------------------------------------------------------- /Parchment/Protocols/PagingViewControllerInfiniteDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// The `PagingViewControllerInfiniteDataSource` protocol is used to 4 | /// provide the `PagingItem` you want to display and which view 5 | /// controller it is associated with. 6 | /// 7 | /// In order for these methods to be called, you first need to set 8 | /// the initial `PagingItem` by calling `select(pagingItem:)` on 9 | /// `PagingViewController`. 10 | public protocol PagingViewControllerInfiniteDataSource: AnyObject { 11 | /// Return the view controller accociated with a `PagingItem`. This 12 | /// method is only called for the currently selected `PagingItem`, 13 | /// and its two possible siblings. 14 | /// 15 | /// - Parameter pagingViewController: The `PagingViewController` 16 | /// instance 17 | /// - Parameter viewControllerForPagingItem: A `PagingItem` instance 18 | /// - Returns: The view controller for the `PagingItem` instance 19 | func pagingViewController(_: PagingViewController, viewControllerFor pagingItem: PagingItem) -> UIViewController 20 | 21 | /// The `PagingItem` that comes before a given `PagingItem` 22 | /// 23 | /// - Parameter pagingViewController: The `PagingViewController` 24 | /// instance 25 | /// - Parameter pagingItemBeforePagingItem: A `PagingItem` instance 26 | /// - Returns: The `PagingItem` that appears before the given 27 | /// `PagingItem`, or `nil` to indicate that no more progress can be 28 | /// made in that direction. 29 | func pagingViewController(_: PagingViewController, itemBefore pagingItem: PagingItem) -> PagingItem? 30 | 31 | /// The `PagingItem` that comes after a given `PagingItem` 32 | /// 33 | /// - Parameter pagingViewController: The `PagingViewController` 34 | /// instance 35 | /// - Parameter pagingItemAfterPagingItem: A `PagingItem` instance 36 | /// - Returns: The `PagingItem` that appears after the given 37 | /// `PagingItem`, or `nil` to indicate that no more progress can be 38 | /// made in that direction. 39 | func pagingViewController(_: PagingViewController, itemAfter pagingItem: PagingItem) -> PagingItem? 40 | } 41 | -------------------------------------------------------------------------------- /ParchmentTests/PagingIndicatorLayoutAttributesTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | import XCTest 4 | 5 | final class PagingIndicatorLayoutAttributesTests: XCTestCase { 6 | let layoutAttributes = PagingIndicatorLayoutAttributes() 7 | var options = PagingOptions() 8 | 9 | override func setUp() { 10 | options.font = UIFont.systemFont(ofSize: 15) 11 | options.selectedFont = UIFont.boldSystemFont(ofSize: 15) 12 | options.textColor = .blue 13 | options.selectedTextColor = .red 14 | options.indicatorColor = .green 15 | options.indicatorOptions = .visible( 16 | height: 20, 17 | zIndex: Int.max, 18 | spacing: UIEdgeInsets(), 19 | insets: UIEdgeInsets() 20 | ) 21 | } 22 | 23 | func testConfigure() { 24 | layoutAttributes.configure(options) 25 | 26 | XCTAssertEqual(layoutAttributes.backgroundColor, UIColor.green) 27 | XCTAssertEqual(layoutAttributes.frame.height, 20) 28 | XCTAssertEqual(layoutAttributes.frame.origin.y, 20) 29 | XCTAssertEqual(layoutAttributes.zIndex, Int.max) 30 | } 31 | 32 | func testTweening() { 33 | layoutAttributes.configure(options) 34 | 35 | let from = PagingIndicatorMetric( 36 | frame: CGRect(x: 0, y: 0, width: 200, height: 0), 37 | insets: .left(50), 38 | spacing: UIEdgeInsets() 39 | ) 40 | 41 | let to = PagingIndicatorMetric( 42 | frame: CGRect(x: 200, y: 0, width: 100, height: 0), 43 | insets: .right(50), 44 | spacing: UIEdgeInsets() 45 | ) 46 | 47 | layoutAttributes.update(from: from, to: to, progress: 0) 48 | XCTAssertEqual(layoutAttributes.frame, CGRect(x: 50, y: 20, width: 150, height: 20)) 49 | 50 | layoutAttributes.update(from: from, to: to, progress: 1) 51 | XCTAssertEqual(layoutAttributes.frame, CGRect(x: 200, y: 20, width: 50, height: 20)) 52 | 53 | layoutAttributes.update(from: from, to: to, progress: 0.5) 54 | XCTAssertEqual(layoutAttributes.frame, CGRect(x: 125, y: 20, width: 100, height: 20)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ParchmentTests/Utilities/CreateDistance.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | 4 | func createDistance( 5 | bounds: CGRect = .zero, 6 | contentSize: CGSize? = nil, 7 | currentItem: Item, 8 | currentItemBounds: CGRect?, 9 | upcomingItem: Item, 10 | upcomingItemBounds: CGRect, 11 | sizeCache: PagingSizeCache, 12 | selectedScrollPosition: PagingSelectedScrollPosition, 13 | navigationOrientation: PagingNavigationOrientation 14 | ) -> PagingDistance { 15 | let collectionView = MockCollectionView() 16 | collectionView.contentOffset = bounds.origin 17 | collectionView.bounds = bounds 18 | 19 | if let contentSize = contentSize { 20 | collectionView.contentSize = contentSize 21 | } else if let currentItemBounds = currentItemBounds { 22 | let contentFrame = currentItemBounds.union(upcomingItemBounds) 23 | collectionView.contentSize = contentFrame.size 24 | } else { 25 | collectionView.contentSize = upcomingItemBounds.size 26 | } 27 | 28 | let visibleItems = PagingItems(items: [currentItem, upcomingItem]) 29 | var layoutAttributes: [IndexPath: PagingCellLayoutAttributes] = [:] 30 | 31 | if let currentItemBounds = currentItemBounds { 32 | let currentIndexPath = visibleItems.indexPath(for: currentItem)! 33 | let currentAttributes = PagingCellLayoutAttributes(forCellWith: currentIndexPath) 34 | currentAttributes.frame = currentItemBounds 35 | layoutAttributes[currentIndexPath] = currentAttributes 36 | } 37 | 38 | let upcomingIndexPath = visibleItems.indexPath(for: upcomingItem)! 39 | let upcomingAttributes = PagingCellLayoutAttributes(forCellWith: upcomingIndexPath) 40 | upcomingAttributes.frame = upcomingItemBounds 41 | layoutAttributes[upcomingIndexPath] = upcomingAttributes 42 | 43 | return PagingDistance( 44 | view: collectionView, 45 | currentPagingItem: currentItem, 46 | upcomingPagingItem: upcomingItem, 47 | visibleItems: visibleItems, 48 | sizeCache: sizeCache, 49 | selectedScrollPosition: selectedScrollPosition, 50 | layoutAttributes: layoutAttributes, 51 | navigationOrientation: navigationOrientation 52 | )! 53 | } 54 | -------------------------------------------------------------------------------- /Example/Examples/Images/ImagePagingCell.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | class ImagePagingCell: PagingCell { 5 | fileprivate lazy var imageView: UIImageView = { 6 | let imageView = UIImageView(frame: .zero) 7 | imageView.contentMode = .scaleAspectFill 8 | return imageView 9 | }() 10 | 11 | fileprivate lazy var titleLabel: UILabel = { 12 | let label = UILabel(frame: .zero) 13 | label.font = UIFont.systemFont(ofSize: 13, weight: UIFont.Weight.semibold) 14 | label.textColor = UIColor.white 15 | label.backgroundColor = UIColor(white: 0, alpha: 0.6) 16 | label.numberOfLines = 0 17 | return label 18 | }() 19 | 20 | fileprivate lazy var paragraphStyle: NSParagraphStyle = { 21 | let paragraphStyle = NSMutableParagraphStyle() 22 | paragraphStyle.hyphenationFactor = 1 23 | paragraphStyle.alignment = .center 24 | return paragraphStyle 25 | }() 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | contentView.layer.cornerRadius = 6 30 | contentView.clipsToBounds = true 31 | contentView.addSubview(imageView) 32 | contentView.addSubview(titleLabel) 33 | contentView.constrainToEdges(imageView) 34 | contentView.constrainToEdges(titleLabel) 35 | } 36 | 37 | required init?(coder _: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options _: PagingOptions) { 42 | let item = pagingItem as! ImageItem 43 | imageView.image = item.headerImage 44 | titleLabel.attributedText = NSAttributedString( 45 | string: item.title, 46 | attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle] 47 | ) 48 | 49 | if selected { 50 | imageView.transform = CGAffineTransform(scaleX: 2, y: 2) 51 | } else { 52 | imageView.transform = CGAffineTransform.identity 53 | } 54 | } 55 | 56 | open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 57 | if let attributes = layoutAttributes as? PagingCellLayoutAttributes { 58 | let scale = 1 + attributes.progress 59 | imageView.transform = CGAffineTransform(scaleX: scale, y: scale) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Parchment/Structs/PagingDiff.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PagingDiff { 4 | private let from: PagingItems 5 | private let to: PagingItems 6 | private var fromCache: [Int: PagingItem] 7 | private var toCache: [Int: PagingItem] 8 | private var lastMatchingItem: PagingItem? 9 | 10 | init(from: PagingItems, to: PagingItems) { 11 | self.from = from 12 | self.to = to 13 | fromCache = [:] 14 | toCache = [:] 15 | 16 | for item in from.items { 17 | fromCache[item.identifier] = item 18 | } 19 | 20 | for item in to.items { 21 | toCache[item.identifier] = item 22 | } 23 | 24 | for toItem in to.items { 25 | for fromItem in from.items { 26 | if toItem.isEqual(to: fromItem) { 27 | lastMatchingItem = toItem 28 | break 29 | } 30 | } 31 | } 32 | } 33 | 34 | func removed() -> [IndexPath] { 35 | let removed = diff(visibleItems: from, cache: toCache) 36 | var items: [IndexPath] = [] 37 | 38 | if let lastItem = lastMatchingItem { 39 | for indexPath in removed { 40 | if let lastIndexPath = from.indexPath(for: lastItem) { 41 | if indexPath.item < lastIndexPath.item { 42 | items.append(indexPath) 43 | } 44 | } 45 | } 46 | } 47 | 48 | return items 49 | } 50 | 51 | func added() -> [IndexPath] { 52 | let removedCount = removed().count 53 | let added = diff(visibleItems: to, cache: fromCache) 54 | 55 | var items: [IndexPath] = [] 56 | 57 | if let lastItem = lastMatchingItem { 58 | for indexPath in added { 59 | if let lastIndexPath = from.indexPath(for: lastItem) { 60 | if indexPath.item + removedCount <= lastIndexPath.item { 61 | items.append(indexPath) 62 | } 63 | } 64 | } 65 | } 66 | 67 | return items 68 | } 69 | 70 | private func diff(visibleItems: PagingItems, cache: [Int: PagingItem]) -> [IndexPath] { 71 | return visibleItems.items.compactMap { item in 72 | if cache[item.identifier] == nil { 73 | return visibleItems.indexPath(for: item) 74 | } 75 | return nil 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingSizeCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class PagingSizeCache { 5 | var options: PagingOptions 6 | var implementsSizeDelegate: Bool = false 7 | var sizeForPagingItem: ((PagingItem, Bool) -> CGFloat?)? 8 | 9 | private var sizeCache: [Int: CGFloat] = [:] 10 | private var selectedSizeCache: [Int: CGFloat] = [:] 11 | 12 | init(options: PagingOptions) { 13 | self.options = options 14 | 15 | let didEnterBackground = UIApplication.didEnterBackgroundNotification 16 | let didReceiveMemoryWarning = UIApplication.didReceiveMemoryWarningNotification 17 | 18 | NotificationCenter.default.addObserver(self, 19 | selector: #selector(applicationDidEnterBackground(notification:)), 20 | name: didEnterBackground, 21 | object: nil) 22 | 23 | NotificationCenter.default.addObserver(self, 24 | selector: #selector(didReceiveMemoryWarning(notification:)), 25 | name: didReceiveMemoryWarning, 26 | object: nil) 27 | } 28 | 29 | deinit { 30 | NotificationCenter.default.removeObserver(self) 31 | } 32 | 33 | func clear() { 34 | sizeCache = [:] 35 | selectedSizeCache = [:] 36 | } 37 | 38 | func itemSize(for pagingItem: PagingItem) -> CGFloat { 39 | if let size = sizeCache[pagingItem.identifier] { 40 | return size 41 | } else { 42 | let size = sizeForPagingItem?(pagingItem, false) 43 | sizeCache[pagingItem.identifier] = size 44 | return size ?? options.estimatedItemWidth 45 | } 46 | } 47 | 48 | func itemWidthSelected(for pagingItem: PagingItem) -> CGFloat { 49 | if let size = selectedSizeCache[pagingItem.identifier] { 50 | return size 51 | } else { 52 | let size = sizeForPagingItem?(pagingItem, true) 53 | selectedSizeCache[pagingItem.identifier] = size 54 | return size ?? options.estimatedItemWidth 55 | } 56 | } 57 | 58 | @objc private func didReceiveMemoryWarning(notification _: NSNotification) { 59 | clear() 60 | } 61 | 62 | @objc private func applicationDidEnterBackground(notification _: NSNotification) { 63 | clear() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Parchment/Protocols/PageViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// The `PageViewControllerDelegate` protocol defines methods that 4 | /// can used to determine when the user navigates between view 5 | /// controllers. 6 | public protocol PageViewControllerDelegate: AnyObject { 7 | /// Called whenever the user is about to start scrolling to a view 8 | /// controller. 9 | /// 10 | /// - Parameters: 11 | /// - pageViewController: The `PageViewController` instance. 12 | /// - startingViewController: The view controller the user is 13 | /// scrolling from. 14 | /// - destinationViewController: The view controller the user is 15 | /// scrolling towards. 16 | func pageViewController( 17 | _ pageViewController: PageViewController, 18 | willStartScrollingFrom startingViewController: UIViewController, 19 | destinationViewController: UIViewController 20 | ) 21 | 22 | /// Called whenever a scroll transition is in progress. 23 | /// 24 | /// - Parameters: 25 | /// - pageViewController: The `PageViewController` instance. 26 | /// - startingViewController: The view controller the user is 27 | /// scrolling from. 28 | /// - destinationViewController: The view controller the user is 29 | /// scrolling towards. Will be nil if the user is scrolling 30 | /// towards one of the edges. 31 | /// - progress: The progress of the scroll transition. Between 0 32 | /// and 1. 33 | func pageViewController( 34 | _ pageViewController: PageViewController, 35 | isScrollingFrom startingViewController: UIViewController, 36 | destinationViewController: UIViewController?, 37 | progress: CGFloat 38 | ) 39 | 40 | /// Called when the user finished scrolling to a new view. 41 | /// 42 | /// - Parameters: 43 | /// - pageViewController: The `PageViewController` instance. 44 | /// - startingViewController: The view controller the user is 45 | /// scrolling from. 46 | /// - destinationViewController: The view controller the user is 47 | /// scrolling towards. 48 | /// - transitionSuccessful: A boolean indicating whether the 49 | /// transition completed, or was cancelled by the user. 50 | func pageViewController( 51 | _ pageViewController: PageViewController, 52 | didFinishScrollingFrom startingViewController: UIViewController, 53 | destinationViewController: UIViewController, 54 | transitionSuccessful: Bool 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /Parchment/Protocols/CollectionView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol CollectionViewLayout: AnyObject { 4 | var state: PagingState { get set } 5 | var visibleItems: PagingItems { get set } 6 | var sizeCache: PagingSizeCache? { get set } 7 | var contentInsets: UIEdgeInsets { get } 8 | var layoutAttributes: [IndexPath: PagingCellLayoutAttributes] { get } 9 | func prepare() 10 | func invalidateLayout() 11 | func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) 12 | } 13 | 14 | extension PagingCollectionViewLayout: CollectionViewLayout {} 15 | 16 | protocol CollectionView: AnyObject { 17 | var indexPathsForVisibleItems: [IndexPath] { get } 18 | var isDragging: Bool { get } 19 | var window: UIWindow? { get } 20 | var superview: UIView? { get } 21 | var bounds: CGRect { get } 22 | var contentOffset: CGPoint { get set } 23 | var contentSize: CGSize { get } 24 | var contentInset: UIEdgeInsets { get } 25 | var showsHorizontalScrollIndicator: Bool { get set } 26 | var dataSource: UICollectionViewDataSource? { get set } 27 | var isScrollEnabled: Bool { get set } 28 | var alwaysBounceHorizontal: Bool { get set } 29 | 30 | @available(iOS 11.0, *) 31 | var contentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior { get set } 32 | 33 | func register(_ cellClass: AnyClass?, forCellWithReuseIdentifier: String) 34 | func register(_ nib: UINib?, forCellWithReuseIdentifier: String) 35 | func addGestureRecognizer(_ recognizer: UIGestureRecognizer) 36 | func removeGestureRecognizer(_ recognizer: UIGestureRecognizer) 37 | func reloadData() 38 | func layoutIfNeeded() 39 | func setContentOffset(_ contentOffset: CGPoint, animated: Bool) 40 | func selectItem(at indexPath: IndexPath?, animated: Bool, scrollPosition: UICollectionView.ScrollPosition) 41 | } 42 | 43 | extension UICollectionView: CollectionView {} 44 | 45 | enum Edge { 46 | case left, right, top, bottom 47 | } 48 | 49 | extension CollectionView { 50 | func near(edge: Edge, clearance: CGFloat = 0) -> Bool { 51 | switch edge { 52 | case .left: 53 | return contentOffset.x + contentInset.left - clearance <= 0 54 | case .right: 55 | return (contentOffset.x + bounds.width + clearance) >= contentSize.width 56 | case .top: 57 | return contentOffset.y + contentInset.top - clearance <= 0 58 | case .bottom: 59 | return (contentOffset.y + bounds.height + clearance) >= contentSize.height 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/MockPageViewManagerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | import XCTest 4 | 5 | final class MockPageViewManagerDelegate: PageViewManagerDelegate { 6 | enum Call: Equatable { 7 | case scrollForward 8 | case scrollReverse 9 | case layoutViews([UIViewController]) 10 | case addViewController(UIViewController) 11 | case removeViewController(UIViewController) 12 | case beginAppearanceTransition(Bool, UIViewController, Bool) 13 | case endAppearanceTransition(UIViewController) 14 | case willScroll(from: UIViewController, to: UIViewController) 15 | case isScrolling(from: UIViewController, to: UIViewController?, progress: CGFloat) 16 | case didFinishScrolling(from: UIViewController, to: UIViewController, success: Bool) 17 | } 18 | 19 | var calls: [Call] = [] 20 | 21 | func scrollForward() { 22 | calls.append(.scrollForward) 23 | } 24 | 25 | func scrollReverse() { 26 | calls.append(.scrollReverse) 27 | } 28 | 29 | func layoutViews(for viewControllers: [UIViewController], keepContentOffset _: Bool) { 30 | calls.append(.layoutViews(viewControllers)) 31 | } 32 | 33 | func addViewController(_ viewController: UIViewController) { 34 | calls.append(.addViewController(viewController)) 35 | } 36 | 37 | func removeViewController(_ viewController: UIViewController) { 38 | calls.append(.removeViewController(viewController)) 39 | } 40 | 41 | func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController, animated: Bool) { 42 | calls.append(.beginAppearanceTransition(isAppearing, viewController, animated)) 43 | } 44 | 45 | func endAppearanceTransition(viewController: UIViewController) { 46 | calls.append(.endAppearanceTransition(viewController)) 47 | } 48 | 49 | func willScroll(from selectedViewController: UIViewController, to destinationViewController: UIViewController) { 50 | calls.append(.willScroll(from: selectedViewController, to: destinationViewController)) 51 | } 52 | 53 | func isScrolling(from selectedViewController: UIViewController, to destinationViewController: UIViewController?, progress: CGFloat) { 54 | calls.append(.isScrolling(from: selectedViewController, to: destinationViewController, progress: progress)) 55 | } 56 | 57 | func didFinishScrolling(from selectedViewController: UIViewController, to destinationViewController: UIViewController, transitionSuccessful: Bool) { 58 | calls.append(.didFinishScrolling(from: selectedViewController, to: destinationViewController, success: transitionSuccessful)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Example/Examples/Images/ImagesViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | protocol ImagesViewControllerDelegate: AnyObject { 5 | func imagesViewControllerDidScroll(_: ImagesViewController) 6 | } 7 | 8 | class ImagesViewController: UIViewController { 9 | weak var delegate: ImagesViewControllerDelegate? 10 | 11 | fileprivate let images: [UIImage] 12 | 13 | fileprivate lazy var collectionViewLayout: UICollectionViewFlowLayout = { 14 | let layout = UICollectionViewFlowLayout() 15 | layout.sectionInset = UIEdgeInsets(top: 18, left: 0, bottom: 18, right: 0) 16 | layout.minimumLineSpacing = 15 17 | return layout 18 | }() 19 | 20 | lazy var collectionView: UICollectionView = { 21 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: self.collectionViewLayout) 22 | collectionView.backgroundColor = .white 23 | return collectionView 24 | }() 25 | 26 | init(images: [UIImage], options _: PagingOptions) { 27 | self.images = images 28 | super.init(nibName: nil, bundle: nil) 29 | 30 | view.addSubview(collectionView) 31 | view.constrainToEdges(collectionView) 32 | 33 | collectionView.dataSource = self 34 | collectionView.delegate = self 35 | collectionView.register( 36 | ImageCollectionViewCell.self, 37 | forCellWithReuseIdentifier: ImageCollectionViewCell.reuseIdentifier 38 | ) 39 | } 40 | 41 | required init?(coder _: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | override func viewWillLayoutSubviews() { 46 | super.viewWillLayoutSubviews() 47 | collectionViewLayout.invalidateLayout() 48 | } 49 | } 50 | 51 | extension ImagesViewController: UICollectionViewDelegateFlowLayout { 52 | func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize { 53 | return CGSize( 54 | width: collectionView.bounds.width - 36, 55 | height: 220 56 | ) 57 | } 58 | 59 | func scrollViewDidScroll(_: UIScrollView) { 60 | delegate?.imagesViewControllerDidScroll(self) 61 | } 62 | } 63 | 64 | extension ImagesViewController: UICollectionViewDataSource { 65 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 66 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCollectionViewCell.reuseIdentifier, for: indexPath) as! ImageCollectionViewCell 67 | cell.setImage(images[indexPath.item]) 68 | return cell 69 | } 70 | 71 | func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { 72 | return images.count 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/Examples/NavigationBar/NavigationBarViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | // Create our own custom paging view and override the layout 5 | // constraints. The default implementation positions the menu view 6 | // above the page view controller, but since we're going to put the 7 | // menu view inside the navigation bar we don't want to setup any 8 | // layout constraints for the menu view. 9 | class NavigationBarPagingView: PagingView { 10 | override func setupConstraints() { 11 | // Use our convenience extension to constrain the page view to all 12 | // of the edges of the super view. 13 | constrainToEdges(pageView) 14 | } 15 | } 16 | 17 | // Create a custom paging view controller and override the view with 18 | // our own custom subclass. 19 | class NavigationBarPagingViewController: PagingViewController { 20 | override func loadView() { 21 | view = NavigationBarPagingView( 22 | options: options, 23 | collectionView: collectionView, 24 | pageView: pageViewController.view 25 | ) 26 | } 27 | } 28 | 29 | class NavigationBarViewController: UIViewController { 30 | let pagingViewController = NavigationBarPagingViewController(viewControllers: [ 31 | ContentViewController(index: 0), 32 | ContentViewController(index: 1), 33 | ContentViewController(index: 2), 34 | ContentViewController(index: 3), 35 | ContentViewController(index: 4), 36 | ]) 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | pagingViewController.borderOptions = .hidden 42 | pagingViewController.menuBackgroundColor = .clear 43 | pagingViewController.indicatorColor = UIColor(white: 0, alpha: 0.4) 44 | pagingViewController.textColor = UIColor(white: 1, alpha: 0.6) 45 | pagingViewController.selectedTextColor = .white 46 | 47 | // Make sure you add the PagingViewController as a child view 48 | // controller and contrain it to the edges of the view. 49 | addChild(pagingViewController) 50 | view.addSubview(pagingViewController.view) 51 | view.constrainToEdges(pagingViewController.view) 52 | pagingViewController.didMove(toParent: self) 53 | 54 | // Set the menu view as the title view on the navigation bar. This 55 | // will remove the menu view from the view hierachy and put it 56 | // into the navigation bar. 57 | navigationItem.titleView = pagingViewController.collectionView 58 | } 59 | 60 | override func viewDidLayoutSubviews() { 61 | super.viewDidLayoutSubviews() 62 | guard let navigationBar = navigationController?.navigationBar else { return } 63 | navigationItem.titleView?.frame = CGRect(origin: .zero, size: navigationBar.bounds.size) 64 | pagingViewController.menuItemSize = .fixed(width: 100, height: navigationBar.bounds.height) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Parchment/Enums/PagingState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// The current state of the menu items. Indicates whether an item 5 | /// is currently selected or is scrolling to another item. Can be 6 | /// used to get the distance and progress of any ongoing transition. 7 | public enum PagingState: Equatable { 8 | case empty 9 | case selected(pagingItem: PagingItem) 10 | case scrolling( 11 | pagingItem: PagingItem, 12 | upcomingPagingItem: PagingItem?, 13 | progress: CGFloat, 14 | initialContentOffset: CGPoint, 15 | distance: CGFloat 16 | ) 17 | } 18 | 19 | public extension PagingState { 20 | var currentPagingItem: PagingItem? { 21 | switch self { 22 | case .empty: 23 | return nil 24 | case let .scrolling(pagingItem, _, _, _, _): 25 | return pagingItem 26 | case let .selected(pagingItem): 27 | return pagingItem 28 | } 29 | } 30 | 31 | var upcomingPagingItem: PagingItem? { 32 | switch self { 33 | case .empty: 34 | return nil 35 | case let .scrolling(_, upcomingPagingItem, _, _, _): 36 | return upcomingPagingItem 37 | case .selected: 38 | return nil 39 | } 40 | } 41 | 42 | var progress: CGFloat { 43 | switch self { 44 | case let .scrolling(_, _, progress, _, _): 45 | return progress 46 | case .selected, .empty: 47 | return 0 48 | } 49 | } 50 | 51 | var distance: CGFloat { 52 | switch self { 53 | case let .scrolling(_, _, _, _, distance): 54 | return distance 55 | case .selected, .empty: 56 | return 0 57 | } 58 | } 59 | 60 | var visuallySelectedPagingItem: PagingItem? { 61 | if abs(progress) > 0.5 { 62 | return upcomingPagingItem ?? currentPagingItem 63 | } else { 64 | return currentPagingItem 65 | } 66 | } 67 | } 68 | 69 | public func == (lhs: PagingState, rhs: PagingState) -> Bool { 70 | switch (lhs, rhs) { 71 | case 72 | (let .scrolling(lhsCurrent, lhsUpcoming, lhsProgress, lhsOffset, lhsDistance), 73 | let .scrolling(rhsCurrent, rhsUpcoming, rhsProgress, rhsOffset, rhsDistance)): 74 | if lhsCurrent.isEqual(to: rhsCurrent), 75 | lhsProgress == rhsProgress, 76 | lhsOffset == rhsOffset, 77 | lhsDistance == rhsDistance { 78 | if let lhsUpcoming = lhsUpcoming, let rhsUpcoming = rhsUpcoming, lhsUpcoming.isEqual(to: rhsUpcoming) { 79 | return true 80 | } else if lhsUpcoming == nil, rhsUpcoming == nil { 81 | return true 82 | } 83 | } 84 | return false 85 | case let (.selected(a), .selected(b)) where a.isEqual(to: b): 86 | return true 87 | case (.empty, .empty): 88 | return true 89 | default: 90 | return false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Example/Examples/Icons/IconsViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | struct IconItem: PagingItem, Hashable { 5 | let icon: String 6 | let index: Int 7 | let image: UIImage? 8 | 9 | init(icon: String, index: Int) { 10 | self.icon = icon 11 | self.index = index 12 | image = UIImage(named: icon) 13 | } 14 | 15 | /// By default, isBefore is implemented when the PagingItem conforms 16 | /// to Comparable, but in this case we want a custom implementation 17 | /// where we also compare IconItem with PagingIndexItem. This 18 | /// ensures that we animate the page transition in the correct 19 | /// direction when selecting items. 20 | func isBefore(item: PagingItem) -> Bool { 21 | if let item = item as? PagingIndexItem { 22 | return index < item.index 23 | } else if let item = item as? Self { 24 | return index < item.index 25 | } else { 26 | return false 27 | } 28 | } 29 | } 30 | 31 | class IconsViewController: UIViewController { 32 | // Let's start by creating an array of icon names that 33 | // we will use to generate some view controllers. 34 | fileprivate let icons = [ 35 | "compass", 36 | "cloud", 37 | "bonnet", 38 | "axe", 39 | "earth", 40 | "knife", 41 | "leave", 42 | "light", 43 | "map", 44 | "moon", 45 | "mushroom", 46 | "shoes", 47 | "snow", 48 | "star", 49 | "sun", 50 | "tipi", 51 | "tree", 52 | "water", 53 | "wind", 54 | "wood", 55 | ] 56 | 57 | override func viewDidLoad() { 58 | super.viewDidLoad() 59 | 60 | let pagingViewController = PagingViewController() 61 | pagingViewController.register(IconPagingCell.self, for: IconItem.self) 62 | pagingViewController.menuItemSize = .fixed(width: 60, height: 60) 63 | pagingViewController.dataSource = self 64 | pagingViewController.select(pagingItem: IconItem(icon: icons[0], index: 0)) 65 | 66 | // Add the paging view controller as a child view controller 67 | // and contrain it to all edges. 68 | addChild(pagingViewController) 69 | view.addSubview(pagingViewController.view) 70 | view.constrainToEdges(pagingViewController.view) 71 | pagingViewController.didMove(toParent: self) 72 | } 73 | } 74 | 75 | extension IconsViewController: PagingViewControllerDataSource { 76 | func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController { 77 | return IconViewController(title: icons[index].capitalized) 78 | } 79 | 80 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { 81 | return IconItem(icon: icons[index], index: index) 82 | } 83 | 84 | func numberOfViewControllers(in _: PagingViewController) -> Int { 85 | return icons.count 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Example/Examples/PageViewController/PageViewExampleViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | /// Parchment provides a custom `UIPageViewController` alternative 5 | /// if you need better delegate methods called `PageViewController`. 6 | class PageViewExampleViewController: UIViewController { 7 | let viewControllers: [UIViewController] = [ 8 | ContentViewController(index: 0), 9 | ContentViewController(index: 1), 10 | ContentViewController(index: 2), 11 | ContentViewController(index: 3), 12 | ContentViewController(index: 4), 13 | ] 14 | 15 | override func viewDidLoad() { 16 | let pageViewController = PageViewController() 17 | pageViewController.dataSource = self 18 | pageViewController.delegate = self 19 | pageViewController.selectViewController(viewControllers[0], direction: .none) 20 | 21 | addChild(pageViewController) 22 | view.addSubview(pageViewController.view) 23 | view.constrainToEdges(pageViewController.view) 24 | pageViewController.didMove(toParent: self) 25 | } 26 | } 27 | 28 | extension PageViewExampleViewController: PageViewControllerDataSource { 29 | func pageViewController( 30 | _: PageViewController, 31 | viewControllerBeforeViewController viewController: UIViewController 32 | ) -> UIViewController? { 33 | guard let index = viewControllers.firstIndex(of: viewController) else { return nil } 34 | if index > 0 { 35 | return viewControllers[index - 1] 36 | } 37 | return nil 38 | } 39 | 40 | func pageViewController( 41 | _: PageViewController, 42 | viewControllerAfterViewController viewController: UIViewController 43 | ) -> UIViewController? { 44 | guard let index = viewControllers.firstIndex(of: viewController) else { return nil } 45 | if index < viewControllers.count - 1 { 46 | return viewControllers[index + 1] 47 | } 48 | return nil 49 | } 50 | } 51 | 52 | extension PageViewExampleViewController: PageViewControllerDelegate { 53 | func pageViewController(_: PageViewController, willStartScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController) { 54 | print("willStartScrollingFrom: ", 55 | startingViewController.title ?? "", 56 | destinationViewController.title ?? "") 57 | } 58 | 59 | func pageViewController(_: PageViewController, isScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController?, progress: CGFloat) { 60 | print("isScrollingFrom: ", 61 | startingViewController.title ?? "", 62 | destinationViewController?.title ?? "", 63 | progress) 64 | } 65 | 66 | func pageViewController(_: PageViewController, didFinishScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController, transitionSuccessful: Bool) { 67 | print("didFinishScrollingFrom: ", 68 | startingViewController.title ?? "", 69 | destinationViewController.title ?? "", 70 | transitionSuccessful) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Example/Examples/Calendar/CalendarViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | // First thing we need to do is create our own PagingItem that will 5 | // hold our date. We need to make sure it conforms to Hashable and 6 | // Comparable, as that is required by PagingViewController. We also 7 | // cache the formatted date strings for performance. 8 | struct CalendarItem: PagingItem, Hashable, Comparable { 9 | let date: Date 10 | let dateText: String 11 | let weekdayText: String 12 | 13 | init(date: Date) { 14 | self.date = date 15 | dateText = DateFormatters.dateFormatter.string(from: date) 16 | weekdayText = DateFormatters.weekdayFormatter.string(from: date) 17 | } 18 | 19 | static func < (lhs: CalendarItem, rhs: CalendarItem) -> Bool { 20 | return lhs.date < rhs.date 21 | } 22 | } 23 | 24 | class CalendarViewController: UIViewController { 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | let pagingViewController = PagingViewController() 29 | pagingViewController.register(CalendarPagingCell.self, for: CalendarItem.self) 30 | pagingViewController.menuItemSize = .fixed(width: 48, height: 58) 31 | pagingViewController.textColor = UIColor.gray 32 | 33 | // Add the paging view controller as a child view 34 | // controller and constrain it to all edges 35 | addChild(pagingViewController) 36 | view.addSubview(pagingViewController.view) 37 | view.constrainToEdges(pagingViewController.view) 38 | pagingViewController.didMove(toParent: self) 39 | 40 | // Set our custom data source 41 | pagingViewController.infiniteDataSource = self 42 | 43 | // Set the current date as the selected paging item 44 | pagingViewController.select(pagingItem: CalendarItem(date: Date())) 45 | } 46 | } 47 | 48 | // We need to conform to PagingViewControllerDataSource in order to 49 | // implement our custom data source. We set the initial item to be the 50 | // current date, and every time pagingItemBeforePagingItem: or 51 | // pagingItemAfterPagingItem: is called, we either subtract or append 52 | // the time interval equal to one day. This means our paging view 53 | // controller will show one menu item for each day. 54 | extension CalendarViewController: PagingViewControllerInfiniteDataSource { 55 | func pagingViewController(_: PagingViewController, itemAfter pagingItem: PagingItem) -> PagingItem? { 56 | let calendarItem = pagingItem as! CalendarItem 57 | return CalendarItem(date: calendarItem.date.addingTimeInterval(86400)) 58 | } 59 | 60 | func pagingViewController(_: PagingViewController, itemBefore pagingItem: PagingItem) -> PagingItem? { 61 | let calendarItem = pagingItem as! CalendarItem 62 | return CalendarItem(date: calendarItem.date.addingTimeInterval(-86400)) 63 | } 64 | 65 | func pagingViewController(_: PagingViewController, viewControllerFor pagingItem: PagingItem) -> UIViewController { 66 | let calendarItem = pagingItem as! CalendarItem 67 | let formattedDate = DateFormatters.shortDateFormatter.string(from: calendarItem.date) 68 | return ContentViewController(title: formattedDate) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Example/Resources/UIView+constraints.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | func constrainCentered(_ subview: UIView) { 5 | subview.translatesAutoresizingMaskIntoConstraints = false 6 | 7 | let verticalContraint = NSLayoutConstraint( 8 | item: subview, 9 | attribute: .centerY, 10 | relatedBy: .equal, 11 | toItem: self, 12 | attribute: .centerY, 13 | multiplier: 1.0, 14 | constant: 0 15 | ) 16 | 17 | let horizontalContraint = NSLayoutConstraint( 18 | item: subview, 19 | attribute: .centerX, 20 | relatedBy: .equal, 21 | toItem: self, 22 | attribute: .centerX, 23 | multiplier: 1.0, 24 | constant: 0 25 | ) 26 | 27 | let heightContraint = NSLayoutConstraint( 28 | item: subview, 29 | attribute: .height, 30 | relatedBy: .equal, 31 | toItem: nil, 32 | attribute: .notAnAttribute, 33 | multiplier: 1.0, 34 | constant: subview.frame.height 35 | ) 36 | 37 | let widthContraint = NSLayoutConstraint( 38 | item: subview, 39 | attribute: .width, 40 | relatedBy: .equal, 41 | toItem: nil, 42 | attribute: .notAnAttribute, 43 | multiplier: 1.0, 44 | constant: subview.frame.width 45 | ) 46 | 47 | addConstraints([ 48 | horizontalContraint, 49 | verticalContraint, 50 | heightContraint, 51 | widthContraint, 52 | ]) 53 | } 54 | 55 | func constrainToEdges(_ subview: UIView) { 56 | subview.translatesAutoresizingMaskIntoConstraints = false 57 | 58 | let topContraint = NSLayoutConstraint( 59 | item: subview, 60 | attribute: .top, 61 | relatedBy: .equal, 62 | toItem: self, 63 | attribute: .top, 64 | multiplier: 1.0, 65 | constant: 0 66 | ) 67 | 68 | let bottomConstraint = NSLayoutConstraint( 69 | item: subview, 70 | attribute: .bottom, 71 | relatedBy: .equal, 72 | toItem: self, 73 | attribute: .bottom, 74 | multiplier: 1.0, 75 | constant: 0 76 | ) 77 | 78 | let leadingContraint = NSLayoutConstraint( 79 | item: subview, 80 | attribute: .leading, 81 | relatedBy: .equal, 82 | toItem: self, 83 | attribute: .leading, 84 | multiplier: 1.0, 85 | constant: 0 86 | ) 87 | 88 | let trailingContraint = NSLayoutConstraint( 89 | item: subview, 90 | attribute: .trailing, 91 | relatedBy: .equal, 92 | toItem: self, 93 | attribute: .trailing, 94 | multiplier: 1.0, 95 | constant: 0 96 | ) 97 | 98 | addConstraints([ 99 | topContraint, 100 | bottomConstraint, 101 | leadingContraint, 102 | trailingContraint, 103 | ]) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Example/Examples/Calendar/CalendarPagingCell.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | class CalendarPagingCell: PagingCell { 5 | private var options: PagingOptions? 6 | 7 | lazy var dateLabel: UILabel = { 8 | let dateLabel = UILabel(frame: .zero) 9 | dateLabel.font = UIFont.systemFont(ofSize: 20) 10 | return dateLabel 11 | }() 12 | 13 | lazy var weekdayLabel: UILabel = { 14 | let weekdayLabel = UILabel(frame: .zero) 15 | weekdayLabel.font = UIFont.systemFont(ofSize: 12) 16 | return weekdayLabel 17 | }() 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | configure() 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | super.init(coder: coder) 26 | configure() 27 | } 28 | 29 | override func layoutSubviews() { 30 | super.layoutSubviews() 31 | let insets = UIEdgeInsets(top: 10, left: 0, bottom: 5, right: 0) 32 | 33 | dateLabel.frame = CGRect( 34 | x: 0, 35 | y: insets.top, 36 | width: contentView.bounds.width, 37 | height: contentView.bounds.midY - insets.top 38 | ) 39 | 40 | weekdayLabel.frame = CGRect( 41 | x: 0, 42 | y: contentView.bounds.midY, 43 | width: contentView.bounds.width, 44 | height: contentView.bounds.midY - insets.bottom 45 | ) 46 | } 47 | 48 | fileprivate func configure() { 49 | weekdayLabel.backgroundColor = .white 50 | weekdayLabel.textAlignment = .center 51 | dateLabel.backgroundColor = .white 52 | dateLabel.textAlignment = .center 53 | 54 | addSubview(weekdayLabel) 55 | addSubview(dateLabel) 56 | } 57 | 58 | fileprivate func updateSelectedState(selected: Bool) { 59 | guard let options = options else { return } 60 | if selected { 61 | dateLabel.textColor = options.selectedTextColor 62 | weekdayLabel.textColor = options.selectedTextColor 63 | } else { 64 | dateLabel.textColor = options.textColor 65 | weekdayLabel.textColor = options.textColor 66 | } 67 | } 68 | 69 | override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options: PagingOptions) { 70 | self.options = options 71 | let calendarItem = pagingItem as! CalendarItem 72 | dateLabel.text = calendarItem.dateText 73 | weekdayLabel.text = calendarItem.weekdayText 74 | 75 | updateSelectedState(selected: selected) 76 | } 77 | 78 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 79 | super.apply(layoutAttributes) 80 | guard let options = options else { return } 81 | 82 | if let attributes = layoutAttributes as? PagingCellLayoutAttributes { 83 | dateLabel.textColor = UIColor.interpolate( 84 | from: options.textColor, 85 | to: options.selectedTextColor, 86 | with: attributes.progress 87 | ) 88 | 89 | weekdayLabel.textColor = UIColor.interpolate( 90 | from: options.textColor, 91 | to: options.selectedTextColor, 92 | with: attributes.progress 93 | ) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Example/Examples/SizeDelegate/SizeDelegateViewController.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | final class SizeDelegateViewController: UIViewController { 5 | // Let's start by creating an array of citites that we 6 | // will use to generate some view controllers. 7 | fileprivate let cities = [ 8 | "Oslo", 9 | "Stockholm", 10 | "Tokyo", 11 | "Barcelona", 12 | "Vancouver", 13 | "Berlin", 14 | "Shanghai", 15 | "London", 16 | "Paris", 17 | "Chicago", 18 | "Madrid", 19 | "Munich", 20 | "Toronto", 21 | "Sydney", 22 | "Melbourne", 23 | ] 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | let pagingViewController = PagingViewController() 29 | pagingViewController.dataSource = self 30 | pagingViewController.sizeDelegate = self 31 | 32 | // Add the paging view controller as a child view controller and 33 | // contrain it to all edges. 34 | addChild(pagingViewController) 35 | view.addSubview(pagingViewController.view) 36 | view.constrainToEdges(pagingViewController.view) 37 | pagingViewController.didMove(toParent: self) 38 | } 39 | } 40 | 41 | extension SizeDelegateViewController: PagingViewControllerDataSource { 42 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { 43 | return PagingIndexItem(index: index, title: cities[index]) 44 | } 45 | 46 | func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController { 47 | return ContentViewController(title: cities[index]) 48 | } 49 | 50 | func numberOfViewControllers(in _: PagingViewController) -> Int { 51 | return cities.count 52 | } 53 | } 54 | 55 | extension SizeDelegateViewController: PagingViewControllerSizeDelegate { 56 | // We want the size of our paging items to equal the width of the 57 | // city title. Parchment does not support self-sizing cells at 58 | // the moment, so we have to handle the calculation ourself. We 59 | // can access the title string by casting the paging item to a 60 | // PagingTitleItem, which is the PagingItem type used by 61 | // FixedPagingViewController. 62 | func pagingViewController(_ pagingViewController: PagingViewController, widthForPagingItem pagingItem: PagingItem, isSelected: Bool) -> CGFloat { 63 | guard let item = pagingItem as? PagingIndexItem else { return 0 } 64 | 65 | let insets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) 66 | let size = CGSize(width: CGFloat.greatestFiniteMagnitude, height: pagingViewController.options.menuItemSize.height) 67 | let attributes = [NSAttributedString.Key.font: pagingViewController.options.font] 68 | 69 | let rect = item.title.boundingRect(with: size, 70 | options: .usesLineFragmentOrigin, 71 | attributes: attributes, 72 | context: nil) 73 | 74 | let width = ceil(rect.width) + insets.left + insets.right 75 | 76 | if isSelected { 77 | return width * 1.5 78 | } else { 79 | return width 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ParchmentTests/PagingStateTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Parchment 3 | import XCTest 4 | 5 | final class PagingStateTests: XCTestCase { 6 | func testSelected() { 7 | let state: PagingState = .selected(pagingItem: Item(index: 0)) 8 | 9 | XCTAssertEqual(state.currentPagingItem as? Item?, Item(index: 0)) 10 | XCTAssertNil(state.upcomingPagingItem) 11 | XCTAssertEqual(state.progress, 0) 12 | XCTAssertEqual(state.visuallySelectedPagingItem as? Item?, Item(index: 0)) 13 | } 14 | 15 | func testScrollingCurrentPagingItem() { 16 | let state: PagingState = .scrolling( 17 | pagingItem: Item(index: 0), 18 | upcomingPagingItem: Item(index: 1), 19 | progress: 0, 20 | initialContentOffset: .zero, 21 | distance: 0 22 | ) 23 | 24 | XCTAssertEqual(state.currentPagingItem as? Item?, Item(index: 0)) 25 | } 26 | 27 | func testProgress() { 28 | let state: PagingState = .scrolling( 29 | pagingItem: Item(index: 0), 30 | upcomingPagingItem: Item(index: 1), 31 | progress: 0.5, 32 | initialContentOffset: .zero, 33 | distance: 0 34 | ) 35 | 36 | XCTAssertEqual(state.progress, 0.5) 37 | } 38 | 39 | func testUpcomingPagingItem() { 40 | let state: PagingState = .scrolling( 41 | pagingItem: Item(index: 0), 42 | upcomingPagingItem: Item(index: 1), 43 | progress: 0, 44 | initialContentOffset: .zero, 45 | distance: 0 46 | ) 47 | 48 | XCTAssertEqual(state.upcomingPagingItem as? Item?, Item(index: 1)) 49 | } 50 | 51 | func testUpcomingPagingItemNil() { 52 | let state: PagingState = .scrolling( 53 | pagingItem: Item(index: 0), 54 | upcomingPagingItem: nil, 55 | progress: 0, 56 | initialContentOffset: .zero, 57 | distance: 0 58 | ) 59 | 60 | XCTAssertNil(state.upcomingPagingItem) 61 | } 62 | 63 | func testVisuallySelectedPagingItemProgressLarge() { 64 | let state: PagingState = .scrolling( 65 | pagingItem: Item(index: 0), 66 | upcomingPagingItem: Item(index: 1), 67 | progress: 0.6, 68 | initialContentOffset: .zero, 69 | distance: 0 70 | ) 71 | 72 | XCTAssertEqual(state.visuallySelectedPagingItem as? Item?, Item(index: 1)) 73 | } 74 | 75 | func testVisuallySelectedPagingItemProgressSmall() { 76 | let state: PagingState = .scrolling( 77 | pagingItem: Item(index: 0), 78 | upcomingPagingItem: Item(index: 1), 79 | progress: 0.3, 80 | initialContentOffset: .zero, 81 | distance: 0 82 | ) 83 | 84 | XCTAssertEqual(state.visuallySelectedPagingItem as? Item?, Item(index: 0)) 85 | } 86 | 87 | func testVisuallySelectedPagingItemUpcomingPagingItemNil() { 88 | let state: PagingState = .scrolling( 89 | pagingItem: Item(index: 0), 90 | upcomingPagingItem: nil, 91 | progress: 0.6, 92 | initialContentOffset: .zero, 93 | distance: 0 94 | ) 95 | 96 | XCTAssertEqual(state.visuallySelectedPagingItem as? Item?, Item(index: 0)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Parchment/Structs/PagingItems.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A data structure used to hold an array of `PagingItem`'s, with 4 | /// methods for getting the index path for a given `PagingItem` and 5 | /// vice versa. 6 | public struct PagingItems { 7 | /// A sorted array of the currently visible `PagingItem`'s. 8 | public let items: [PagingItem] 9 | 10 | let hasItemsBefore: Bool 11 | let hasItemsAfter: Bool 12 | private var cachedItems: [Int: PagingItem] 13 | 14 | init(items: [PagingItem], hasItemsBefore: Bool = false, hasItemsAfter: Bool = false) { 15 | self.items = items 16 | self.hasItemsBefore = hasItemsBefore 17 | self.hasItemsAfter = hasItemsAfter 18 | cachedItems = [:] 19 | 20 | for item in items { 21 | cachedItems[item.identifier] = item 22 | } 23 | } 24 | 25 | /// The `IndexPath` for a given `PagingItem`. Returns nil if the 26 | /// `PagingItem` is not in the `items` array. 27 | /// 28 | /// - Parameter pagingItem: A `PagingItem` instance 29 | /// - Returns: The `IndexPath` for the given `PagingItem` 30 | public func indexPath(for pagingItem: PagingItem) -> IndexPath? { 31 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { return nil } 32 | return IndexPath(item: index, section: 0) 33 | } 34 | 35 | /// The `PagingItem` for a given `IndexPath`. This method will crash 36 | /// if you pass in an `IndexPath` that is currently not visible in 37 | /// the collection view. 38 | /// 39 | /// - Parameter indexPath: An `IndexPath` that is currently visible 40 | /// - Returns: The `PagingItem` for the given `IndexPath` 41 | public func pagingItem(for indexPath: IndexPath) -> PagingItem { 42 | return items[indexPath.item] 43 | } 44 | 45 | /// The direction from a given `PagingItem` to another `PagingItem`. 46 | /// If the `PagingItem`'s are equal the direction will be .none. 47 | /// 48 | /// - Parameter from: The current `PagingItem` 49 | /// - Parameter to: The `PagingItem` being scrolled towards 50 | /// - Returns: The `PagingDirection` for a given `PagingItem` 51 | public func direction(from: PagingItem, to: PagingItem) -> PagingDirection { 52 | if from.isBefore(item: to) { 53 | return .forward(sibling: isSibling(from: from, to: to)) 54 | } else if to.isBefore(item: from) { 55 | return .reverse(sibling: isSibling(from: from, to: to)) 56 | } 57 | return .none 58 | } 59 | 60 | func isSibling(from: PagingItem, to: PagingItem) -> Bool { 61 | guard 62 | let fromIndex = items.firstIndex(where: { $0.isEqual(to: from) }), 63 | let toIndex = items.firstIndex(where: { $0.isEqual(to: to) }) 64 | else { return false } 65 | 66 | if fromIndex == toIndex - 1 { 67 | return true 68 | } else if fromIndex - 1 == toIndex { 69 | return true 70 | } else { 71 | return false 72 | } 73 | } 74 | 75 | func contains(_ pagingItem: PagingItem) -> Bool { 76 | return cachedItems[pagingItem.identifier] != nil ? true : false 77 | } 78 | 79 | func union(_ newItems: [PagingItem]) -> [PagingItem] { 80 | let old = Set(items.map { AnyPagingItem(base: $0) }) 81 | let new = Set(newItems.map { AnyPagingItem(base: $0) }) 82 | return Array(old.union(new)) 83 | .map { $0.base } 84 | .sorted(by: { $0.isBefore(item: $1) }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Documentation/infinite-data-source.md: -------------------------------------------------------------------------------- 1 | # Using the infinite data source 2 | 3 | If you’re creating something like a calendar, the number of view controllers can be infinitely large, or maybe you don’t know exactly how many items you need to display since you’re fetching them from the server as you are scrolling. In these cases you can use the `PagingViewControllerInfiniteDataSource` protocol. 4 | 5 | ## Calendar example 6 | 7 | Let’s look at how you can create your own calendar data source. This is what we want to achieve: 8 | 9 | ![](https://rechsteiner-parchment.s3.eu-central-1.amazonaws.com/parchment-calendar.gif "Calendar Example") 10 | 11 | The first thing we need to do is create our own `PagingItem` to hold our dates. `PagingItem` is just an empty protocol that is used to generate menu items for all the view controllers, without having to actually allocate them before they are needed. You can store whatever data that makes the most sense for your application, the only requirement is that it needs to conform to `Hashable` and `Comparable`. 12 | 13 | ```Swift 14 | struct CalendarItem: PagingItem, Hashable, Comparable { 15 | let date: Date 16 | 17 | static func < (lhs: CalendarItem, rhs: CalendarItem) -> Bool { 18 | return lhs.date < rhs.date 19 | } 20 | } 21 | ``` 22 | 23 | Now that we have our custom `PagingItem`, we can create our `PagingViewController` instance: 24 | 25 | ```Swift 26 | class ViewController: UIViewController { 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | let pagingViewController = PagingViewController() 30 | } 31 | } 32 | ``` 33 | 34 | Then we need to conform to the `PagingViewControllerInfiniteDataSource` protocol: 35 | 36 | ```Swift 37 | extension ViewController: PagingViewControllerInfiniteDataSource { 38 | func pagingViewController(_: PagingViewController, itemAfter pagingItem: PagingItem) -> PagingItem? { 39 | let calendarItem = pagingItem as! CalendarItem 40 | return CalendarItem(date: calendarItem.date.addingTimeInterval(86400)) 41 | } 42 | 43 | func pagingViewController(_: PagingViewController, itemBefore pagingItem: PagingItem) -> PagingItem? { 44 | let calendarItem = pagingItem as! CalendarItem 45 | return CalendarItem(date: calendarItem.date.addingTimeInterval(-86400)) 46 | } 47 | 48 | func pagingViewController(_: PagingViewController, viewControllerFor pagingItem: PagingItem) -> UIViewController { 49 | let calendarItem = pagingItem as! CalendarItem 50 | return CalendarViewController(date: calendarItem.date) 51 | } 52 | } 53 | ``` 54 | 55 | Every time `itemBefore:` or `itemAfter:` is called, we either subtract or append the time interval equal to one day. This means our paging view controller will show one menu item for each day. 56 | 57 | We then set our `infiniteDataSource` property and select our initial item. In this example, we want the current date to be the initially selected: 58 | 59 | ```Swift 60 | pagingViewController.infiniteDataSource = self 61 | pagingViewController.select(pagingItem: CalendarItem(date: Date())) 62 | ``` 63 | 64 | Finally, we add `pagingViewController` as a child view controller and setup the constraints for the view: 65 | 66 | ```Swift 67 | addChildViewController(pagingViewController) 68 | view.addSubview(pagingViewController.view) 69 | pagingViewController.didMove(toParentViewController: self) 70 | pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false 71 | 72 | NSLayoutConstraint.activate([ 73 | pagingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 74 | pagingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 75 | pagingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 76 | pagingViewController.view.topAnchor.constraint(equalTo: view.topAnchor) 77 | ]) 78 | ``` 79 | 80 | _Check out the Calendar example target for more details_ 81 | -------------------------------------------------------------------------------- /Parchment.xcodeproj/xcshareddata/xcschemes/Parchment.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 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A custom `UIView` subclass used by `PagingViewController`, 4 | /// responsible for setting up the view hierarchy and its layout 5 | /// constraints. 6 | /// 7 | /// If you need additional customization, like changing the 8 | /// constraints, you can subclass `PagingView` and override 9 | /// `loadView:` in `PagingViewController` to use your subclass. 10 | open class PagingView: UIView { 11 | // MARK: Public Properties 12 | 13 | public let collectionView: UICollectionView 14 | public let pageView: UIView 15 | public var options: PagingOptions { 16 | didSet { 17 | heightConstraint?.constant = options.menuItemSize.height 18 | collectionView.backgroundColor = options.menuBackgroundColor 19 | } 20 | } 21 | 22 | // MARK: Private Properties 23 | 24 | private var heightConstraint: NSLayoutConstraint? 25 | 26 | // MARK: Initializers 27 | 28 | /// Creates an instance of `PagingView`. 29 | /// 30 | /// - Parameter options: The `PagingOptions` passed into the 31 | /// `PagingViewController`. 32 | public init(options: PagingOptions, collectionView: UICollectionView, pageView: UIView) { 33 | self.options = options 34 | self.collectionView = collectionView 35 | self.pageView = pageView 36 | super.init(frame: .zero) 37 | } 38 | 39 | public required init?(coder _: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | // MARK: Public Methods 44 | 45 | /// Configures the view hierarchy, sets up the layout constraints 46 | /// and does any other customization based on the `PagingOptions`. 47 | /// Override this if you need any custom behavior. 48 | open func configure() { 49 | collectionView.backgroundColor = options.menuBackgroundColor 50 | addSubview(pageView) 51 | addSubview(collectionView) 52 | setupConstraints() 53 | } 54 | 55 | /// Sets up all the layout constraints. Override this if you need to 56 | /// make changes to how the views are layed out. 57 | open func setupConstraints() { 58 | collectionView.translatesAutoresizingMaskIntoConstraints = false 59 | pageView.translatesAutoresizingMaskIntoConstraints = false 60 | 61 | let metrics = [ 62 | "height": options.menuHeight, 63 | ] 64 | 65 | let views = [ 66 | "collectionView": collectionView, 67 | "pageView": pageView, 68 | ] 69 | 70 | let formatOptions = NSLayoutConstraint.FormatOptions() 71 | 72 | let horizontalCollectionViewContraints = NSLayoutConstraint.constraints( 73 | withVisualFormat: "H:|[collectionView]|", 74 | options: formatOptions, 75 | metrics: metrics, 76 | views: views 77 | ) 78 | 79 | let horizontalPagingContentViewContraints = NSLayoutConstraint.constraints( 80 | withVisualFormat: "H:|[pageView]|", 81 | options: formatOptions, 82 | metrics: metrics, 83 | views: views 84 | ) 85 | 86 | let verticalConstraintsFormat: String 87 | switch options.menuPosition { 88 | case .top: 89 | verticalConstraintsFormat = "V:|[collectionView(==height)][pageView]|" 90 | case .bottom: 91 | verticalConstraintsFormat = "V:|[pageView][collectionView(==height)]|" 92 | } 93 | 94 | let verticalContraints = NSLayoutConstraint.constraints( 95 | withVisualFormat: verticalConstraintsFormat, 96 | options: formatOptions, 97 | metrics: metrics, 98 | views: views 99 | ) 100 | 101 | addConstraints(horizontalCollectionViewContraints) 102 | addConstraints(horizontalPagingContentViewContraints) 103 | addConstraints(verticalContraints) 104 | 105 | for constraint in verticalContraints { 106 | if constraint.firstAttribute == NSLayoutConstraint.Attribute.height { 107 | heightConstraint = constraint 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Parchment/Protocols/PagingViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// The `PagingViewControllerDelegate` protocol defines methods that 5 | /// can used to determine when the user navigates between view 6 | /// controllers. 7 | public protocol PagingViewControllerDelegate: AnyObject { 8 | /// Called whenever a scroll transition is in progress. 9 | /// 10 | /// - Parameter currentPagingItem: The currently selected `PagingItem` 11 | /// - Parameter upcomingPagingItem: The `PagingItem` being scrolled to 12 | /// - Parameter startingViewController: The view controller for the 13 | /// current paging item 14 | /// - Parameter destinationViewController: The view controller for 15 | /// the upcoming paging item 16 | /// - Parameter progress: The progress of the scroll transition 17 | func pagingViewController( 18 | _: PagingViewController, 19 | isScrollingFromItem currentPagingItem: PagingItem, 20 | toItem upcomingPagingItem: PagingItem?, 21 | startingViewController: UIViewController, 22 | destinationViewController: UIViewController?, 23 | progress: CGFloat 24 | ) 25 | 26 | /// Called whenever a scroll transition is about to start. 27 | /// 28 | /// - Parameter pagingItem: The `PagingItem` being scrolled to 29 | /// - Parameter startingViewController: The view controller for the 30 | /// current paging item 31 | /// - Parameter destinationViewController: The view controller for 32 | /// the upcoming paging item 33 | func pagingViewController( 34 | _: PagingViewController, 35 | willScrollToItem pagingItem: PagingItem, 36 | startingViewController: UIViewController, 37 | destinationViewController: UIViewController 38 | ) 39 | 40 | /// Called whenever a scroll transition completes or is cancelled. 41 | /// 42 | /// - Parameter pagingItem: The `PagingItem` that was scroll to 43 | /// - Parameter startingViewController: The view controller for the 44 | /// current paging item 45 | /// - Parameter destinationViewController: The view controller for 46 | /// the upcoming paging item 47 | /// - Parameter transitionSuccessful: Boolean that indicates whether 48 | /// the transition to the paging item was successful or not 49 | func pagingViewController( 50 | _ pagingViewController: PagingViewController, 51 | didScrollToItem pagingItem: PagingItem, 52 | startingViewController: UIViewController?, 53 | destinationViewController: UIViewController, 54 | transitionSuccessful: Bool 55 | ) 56 | 57 | /// Called when paging cell is selected in the menu. 58 | /// 59 | /// - Parameter pagingViewController: The `PagingViewController` instance 60 | /// - Parameter pagingItem: The item that was selected. 61 | func pagingViewController( 62 | _ pagingViewController: PagingViewController, 63 | didSelectItem pagingItem: PagingItem 64 | ) 65 | } 66 | 67 | public extension PagingViewControllerDelegate { 68 | func pagingViewController( 69 | _: PagingViewController, 70 | isScrollingFromItem _: PagingItem, 71 | toItem _: PagingItem?, 72 | startingViewController _: UIViewController, 73 | destinationViewController _: UIViewController?, 74 | progress _: CGFloat 75 | ) { 76 | return 77 | } 78 | 79 | func pagingViewController( 80 | _: PagingViewController, 81 | willScrollToItem _: PagingItem, 82 | startingViewController _: UIViewController, 83 | destinationViewController _: UIViewController 84 | ) { 85 | return 86 | } 87 | 88 | func pagingViewController( 89 | _: PagingViewController, 90 | didScrollToItem _: PagingItem, 91 | startingViewController _: UIViewController?, 92 | destinationViewController _: UIViewController, 93 | transitionSuccessful _: Bool 94 | ) { 95 | return 96 | } 97 | 98 | func pagingViewController( 99 | _: PagingViewController, 100 | didSelectItem _: PagingItem 101 | ) { 102 | return 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Example/Examples/Icons/IconPagingCell.swift: -------------------------------------------------------------------------------- 1 | import Parchment 2 | import UIKit 3 | 4 | struct IconPagingCellViewModel { 5 | let image: UIImage? 6 | let selected: Bool 7 | let tintColor: UIColor 8 | let selectedTintColor: UIColor 9 | 10 | init(image: UIImage?, selected: Bool, options: PagingOptions) { 11 | self.image = image 12 | self.selected = selected 13 | tintColor = options.textColor 14 | selectedTintColor = options.selectedTextColor 15 | } 16 | } 17 | 18 | class IconPagingCell: PagingCell { 19 | fileprivate var viewModel: IconPagingCellViewModel? 20 | 21 | fileprivate lazy var imageView: UIImageView = { 22 | let imageView = UIImageView(frame: .zero) 23 | imageView.contentMode = .scaleAspectFit 24 | return imageView 25 | }() 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | contentView.addSubview(imageView) 30 | setupConstraints() 31 | } 32 | 33 | required init?(coder _: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options: PagingOptions) { 38 | if let item = pagingItem as? IconItem { 39 | let viewModel = IconPagingCellViewModel( 40 | image: item.image, 41 | selected: selected, 42 | options: options 43 | ) 44 | 45 | imageView.image = viewModel.image 46 | 47 | if viewModel.selected { 48 | imageView.transform = CGAffineTransform(scaleX: 1, y: 1) 49 | imageView.tintColor = viewModel.selectedTintColor 50 | } else { 51 | imageView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6) 52 | imageView.tintColor = viewModel.tintColor 53 | } 54 | 55 | self.viewModel = viewModel 56 | } 57 | } 58 | 59 | open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 60 | guard let viewModel = viewModel else { return } 61 | if let attributes = layoutAttributes as? PagingCellLayoutAttributes { 62 | let scale = (0.4 * attributes.progress) + 0.6 63 | imageView.transform = CGAffineTransform(scaleX: scale, y: scale) 64 | imageView.tintColor = UIColor.interpolate( 65 | from: viewModel.tintColor, 66 | to: viewModel.selectedTintColor, 67 | with: attributes.progress 68 | ) 69 | } 70 | } 71 | 72 | private func setupConstraints() { 73 | imageView.translatesAutoresizingMaskIntoConstraints = false 74 | 75 | let topContraint = NSLayoutConstraint( 76 | item: imageView, 77 | attribute: .top, 78 | relatedBy: .equal, 79 | toItem: contentView, 80 | attribute: .top, 81 | multiplier: 1.0, 82 | constant: 15 83 | ) 84 | 85 | let bottomConstraint = NSLayoutConstraint( 86 | item: imageView, 87 | attribute: .bottom, 88 | relatedBy: .equal, 89 | toItem: contentView, 90 | attribute: .bottom, 91 | multiplier: 1.0, 92 | constant: -15 93 | ) 94 | 95 | let leadingContraint = NSLayoutConstraint( 96 | item: imageView, 97 | attribute: .leading, 98 | relatedBy: .equal, 99 | toItem: contentView, 100 | attribute: .leading, 101 | multiplier: 1.0, 102 | constant: 0 103 | ) 104 | 105 | let trailingContraint = NSLayoutConstraint( 106 | item: imageView, 107 | attribute: .trailing, 108 | relatedBy: .equal, 109 | toItem: contentView, 110 | attribute: .trailing, 111 | multiplier: 1.0, 112 | constant: 0 113 | ) 114 | 115 | contentView.addConstraints([ 116 | topContraint, 117 | bottomConstraint, 118 | leadingContraint, 119 | trailingContraint, 120 | ]) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Parchment/Classes/PagingTitleCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A custom `PagingCell` implementation that only displays a text 4 | /// label. The title is based on the `PagingTitleItem` and the colors 5 | /// are based on the `PagingTheme` passed into `setPagingItem:`. When 6 | /// applying layout attributes it will interpolate between the default 7 | /// and selected text color based on the `progress` property. 8 | open class PagingTitleCell: PagingCell { 9 | public let titleLabel = UILabel(frame: .zero) 10 | private var viewModel: PagingTitleCellViewModel? 11 | 12 | private lazy var horizontalConstraints: [NSLayoutConstraint] = { 13 | NSLayoutConstraint.constraints( 14 | withVisualFormat: "H:|[label]|", 15 | options: NSLayoutConstraint.FormatOptions(), 16 | metrics: nil, 17 | views: ["label": titleLabel] 18 | ) 19 | }() 20 | 21 | private lazy var verticalConstraints: [NSLayoutConstraint] = { 22 | NSLayoutConstraint.constraints( 23 | withVisualFormat: "V:|[label]|", 24 | options: NSLayoutConstraint.FormatOptions(), 25 | metrics: nil, 26 | views: ["label": titleLabel] 27 | ) 28 | }() 29 | 30 | open override var isSelected: Bool { 31 | didSet { 32 | configureTitleLabel() 33 | } 34 | } 35 | 36 | public override init(frame: CGRect) { 37 | super.init(frame: frame) 38 | configure() 39 | } 40 | 41 | public required init?(coder: NSCoder) { 42 | super.init(coder: coder) 43 | configure() 44 | } 45 | 46 | open override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options: PagingOptions) { 47 | if let titleItem = pagingItem as? PagingIndexItem { 48 | viewModel = PagingTitleCellViewModel( 49 | title: titleItem.title, 50 | selected: selected, 51 | options: options 52 | ) 53 | } 54 | configureTitleLabel() 55 | configureAccessibility() 56 | } 57 | 58 | open func configure() { 59 | contentView.addSubview(titleLabel) 60 | contentView.isAccessibilityElement = true 61 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 62 | 63 | contentView.addConstraints(horizontalConstraints) 64 | contentView.addConstraints(verticalConstraints) 65 | } 66 | 67 | open func configureTitleLabel() { 68 | guard let viewModel = viewModel else { return } 69 | titleLabel.text = viewModel.title 70 | titleLabel.textAlignment = .center 71 | 72 | if viewModel.selected { 73 | titleLabel.font = viewModel.selectedFont 74 | titleLabel.textColor = viewModel.selectedTextColor 75 | backgroundColor = viewModel.selectedBackgroundColor 76 | } else { 77 | titleLabel.font = viewModel.font 78 | titleLabel.textColor = viewModel.textColor 79 | backgroundColor = viewModel.backgroundColor 80 | } 81 | 82 | horizontalConstraints.forEach { $0.constant = viewModel.labelSpacing } 83 | } 84 | 85 | open func configureAccessibility() { 86 | accessibilityIdentifier = viewModel?.title 87 | contentView.accessibilityLabel = viewModel?.title 88 | contentView.accessibilityTraits = viewModel?.selected ?? false ? .selected : .none 89 | } 90 | 91 | open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 92 | super.apply(layoutAttributes) 93 | guard let viewModel = viewModel else { return } 94 | if let attributes = layoutAttributes as? PagingCellLayoutAttributes { 95 | titleLabel.textColor = UIColor.interpolate( 96 | from: viewModel.textColor, 97 | to: viewModel.selectedTextColor, 98 | with: attributes.progress 99 | ) 100 | 101 | backgroundColor = UIColor.interpolate( 102 | from: viewModel.backgroundColor, 103 | to: viewModel.selectedBackgroundColor, 104 | with: attributes.progress 105 | ) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ParchmentTests/Mocks/MockCollectionView.swift: -------------------------------------------------------------------------------- 1 | @testable import Parchment 2 | import UIKit 3 | 4 | final class MockCollectionView: CollectionView, Mock { 5 | enum Action: Equatable { 6 | case contentOffset(CGPoint) 7 | case reloadData 8 | case layoutIfNeeded 9 | case setContentOffset( 10 | contentOffset: CGPoint, 11 | animated: Bool 12 | ) 13 | case selectItem( 14 | indexPath: IndexPath?, 15 | animated: Bool, 16 | scrollPosition: UICollectionView.ScrollPosition 17 | ) 18 | } 19 | 20 | var visibleItems: (() -> Int)! 21 | 22 | weak var collectionViewLayout: MockCollectionViewLayout! 23 | 24 | var calls: [MockCall] = [] 25 | var indexPathsForVisibleItems: [IndexPath] = [] 26 | var isDragging: Bool = false 27 | var window: UIWindow? 28 | var superview: UIView? 29 | var bounds: CGRect = .zero 30 | var contentSize: CGSize = .zero 31 | var contentInset: UIEdgeInsets = .zero 32 | var showsHorizontalScrollIndicator: Bool = false 33 | var dataSource: UICollectionViewDataSource? 34 | var isScrollEnabled: Bool = false 35 | var alwaysBounceHorizontal: Bool = false 36 | 37 | private var _contentInsetAdjustmentBehavior: Any? 38 | @available(iOS 11.0, *) 39 | var contentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior { 40 | get { 41 | if _contentInsetAdjustmentBehavior == nil { 42 | return .never 43 | } 44 | return _contentInsetAdjustmentBehavior as! UIScrollView.ContentInsetAdjustmentBehavior 45 | } 46 | set { 47 | _contentInsetAdjustmentBehavior = newValue 48 | } 49 | } 50 | 51 | var contentOffset: CGPoint = .zero { 52 | didSet { 53 | calls.append(MockCall( 54 | datetime: Date(), 55 | action: .collectionView(.contentOffset(contentOffset)) 56 | )) 57 | } 58 | } 59 | 60 | func reloadData() { 61 | calls.append(MockCall( 62 | datetime: Date(), 63 | action: .collectionView(.reloadData) 64 | )) 65 | 66 | let items = visibleItems() 67 | let range = 0 ..< items 68 | let indexPaths = range.map { IndexPath(item: $0, section: 0) } 69 | 70 | contentSize = CGSize( 71 | width: PagingControllerTests.ItemSize * CGFloat(items), 72 | height: PagingControllerTests.ItemSize 73 | ) 74 | indexPathsForVisibleItems = indexPaths 75 | 76 | var layoutAttributes: [IndexPath: PagingCellLayoutAttributes] = [:] 77 | 78 | for indexPath in indexPaths { 79 | let attributes = PagingCellLayoutAttributes(forCellWith: indexPath) 80 | attributes.frame = CGRect( 81 | x: PagingControllerTests.ItemSize * CGFloat(indexPath.item), 82 | y: 0, 83 | width: PagingControllerTests.ItemSize, 84 | height: PagingControllerTests.ItemSize 85 | ) 86 | layoutAttributes[indexPath] = attributes 87 | } 88 | 89 | collectionViewLayout.layoutAttributes = layoutAttributes 90 | } 91 | 92 | func layoutIfNeeded() { 93 | calls.append(MockCall( 94 | datetime: Date(), 95 | action: .collectionView(.layoutIfNeeded) 96 | )) 97 | } 98 | 99 | func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { 100 | calls.append(MockCall( 101 | datetime: Date(), 102 | action: .collectionView(.setContentOffset( 103 | contentOffset: contentOffset, 104 | animated: animated 105 | )) 106 | )) 107 | } 108 | 109 | func selectItem(at indexPath: IndexPath?, animated: Bool, scrollPosition: UICollectionView.ScrollPosition) { 110 | calls.append(MockCall( 111 | datetime: Date(), 112 | action: .collectionView(.selectItem( 113 | indexPath: indexPath, 114 | animated: animated, 115 | scrollPosition: scrollPosition 116 | )) 117 | )) 118 | if let indexPath = indexPath { 119 | contentOffset = CGPoint( 120 | x: CGFloat(indexPath.item) * PagingControllerTests.ItemSize, 121 | y: 0 122 | ) 123 | } 124 | } 125 | 126 | func register(_: AnyClass?, forCellWithReuseIdentifier _: String) { 127 | return 128 | } 129 | 130 | func register(_: UINib?, forCellWithReuseIdentifier _: String) { 131 | return 132 | } 133 | 134 | func addGestureRecognizer(_: UIGestureRecognizer) { 135 | return 136 | } 137 | 138 | func removeGestureRecognizer(_: UIGestureRecognizer) { 139 | return 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Example/ExamplesViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum Example: CaseIterable { 4 | case basic 5 | case selfSizing 6 | case calendar 7 | case sizeDelegate 8 | case images 9 | case icons 10 | case storyboard 11 | case navigationBar 12 | case largeTitles 13 | case scroll 14 | case header 15 | case multipleCells 16 | case pageViewController 17 | 18 | var title: String { 19 | switch self { 20 | case .basic: 21 | return "Basic" 22 | case .selfSizing: 23 | return "Self sizing cells" 24 | case .calendar: 25 | return "Calendar" 26 | case .sizeDelegate: 27 | return "Size delegate" 28 | case .images: 29 | return "Images" 30 | case .icons: 31 | return "Icons" 32 | case .storyboard: 33 | return "Storyboard" 34 | case .navigationBar: 35 | return "Navigation bar" 36 | case .largeTitles: 37 | return "Large titles" 38 | case .scroll: 39 | return "Hide menu on scroll" 40 | case .header: 41 | return "Header above menu" 42 | case .multipleCells: 43 | return "Multiple cells" 44 | case .pageViewController: 45 | return "PageViewController" 46 | } 47 | } 48 | } 49 | 50 | final class ExamplesViewController: UITableViewController { 51 | static let CellIdentifier = "CellIdentifier" 52 | 53 | var isUITesting: Bool { 54 | return ProcessInfo.processInfo.arguments.contains("--ui-testing") 55 | } 56 | 57 | override func viewDidLoad() { 58 | super.viewDidLoad() 59 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: Self.CellIdentifier) 60 | 61 | if isUITesting { 62 | let viewController = createViewController(for: .basic) 63 | navigationController?.setViewControllers([viewController], animated: false) 64 | } 65 | } 66 | 67 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 68 | let cell = tableView.dequeueReusableCell(withIdentifier: Self.CellIdentifier, for: indexPath) 69 | let example = Example.allCases[indexPath.row] 70 | cell.textLabel?.text = example.title 71 | return cell 72 | } 73 | 74 | override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 75 | return Example.allCases.count 76 | } 77 | 78 | override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { 79 | let example = Example.allCases[indexPath.row] 80 | let viewController = createViewController(for: example) 81 | viewController.title = example.title 82 | 83 | switch example { 84 | case .largeTitles: 85 | let navigationController = UINavigationController(rootViewController: viewController) 86 | navigationController.modalPresentationStyle = .fullScreen 87 | viewController.navigationItem.rightBarButtonItem = UIBarButtonItem( 88 | barButtonSystemItem: .done, 89 | target: self, 90 | action: #selector(handleDismiss) 91 | ) 92 | present(navigationController, animated: true) 93 | default: 94 | viewController.view.backgroundColor = .white 95 | navigationController?.pushViewController(viewController, animated: true) 96 | } 97 | } 98 | 99 | private func createViewController(for example: Example) -> UIViewController { 100 | switch example { 101 | case .basic: 102 | return BasicViewController(nibName: nil, bundle: nil) 103 | case .calendar: 104 | return CalendarViewController(nibName: nil, bundle: nil) 105 | case .selfSizing: 106 | return SelfSizingViewController() 107 | case .sizeDelegate: 108 | return SizeDelegateViewController(nibName: nil, bundle: nil) 109 | case .images: 110 | return UnsplashViewController(nibName: nil, bundle: nil) 111 | case .icons: 112 | return IconsViewController(nibName: nil, bundle: nil) 113 | case .storyboard: 114 | return StoryboardViewController(nibName: nil, bundle: nil) 115 | case .navigationBar: 116 | return NavigationBarViewController(nibName: nil, bundle: nil) 117 | case .largeTitles: 118 | return LargeTitlesViewController(nibName: nil, bundle: nil) 119 | case .scroll: 120 | return ScrollViewController(nibName: nil, bundle: nil) 121 | case .header: 122 | return HeaderViewController(nibName: nil, bundle: nil) 123 | case .multipleCells: 124 | return MultipleCellsViewController(nibName: nil, bundle: nil) 125 | case .pageViewController: 126 | return PageViewExampleViewController(nibName: nil, bundle: nil) 127 | } 128 | } 129 | 130 | @objc private func handleDismiss() { 131 | dismiss(animated: true) 132 | } 133 | } 134 | --------------------------------------------------------------------------------