├── .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 | 
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 |
--------------------------------------------------------------------------------