├── .github
└── workflows
│ └── parchment.yml
├── .gitignore
├── .images
├── example-calendar.gif
├── example-cities.gif
├── example-unsplash.gif
├── title-dark-mode.png
└── title-light-mode.png
├── .swift-version
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── CHANGELOG.md
├── Documentation
├── basic-usage.md
├── data-source.md
└── infinite-data-source.md
├── Example
├── AppDelegate.swift
├── Examples
│ ├── Basic
│ │ └── BasicViewController.swift
│ ├── Calendar
│ │ ├── CalendarPagingCell.swift
│ │ ├── CalendarViewController.swift
│ │ └── DateFormatters.swift
│ ├── Header
│ │ └── HeaderViewController.swift
│ ├── Icons
│ │ ├── IconPagingCell.swift
│ │ ├── IconViewController.swift
│ │ └── IconsViewController.swift
│ ├── Images
│ │ ├── ImageCollectionViewCell.swift
│ │ ├── ImagePagingCell.swift
│ │ ├── ImagesViewController.swift
│ │ └── UnsplashViewController.swift
│ ├── LargeTitles
│ │ └── LargeTitlesViewController.swift
│ ├── MultipleCells
│ │ └── MultipleCellsViewController.swift
│ ├── NavigationBar
│ │ └── NavigationBarViewController.swift
│ ├── PageViewController
│ │ └── PageViewExampleViewController.swift
│ ├── Scroll
│ │ └── ScrollViewController.swift
│ ├── SelfSizing
│ │ └── SelfSizingViewController.swift
│ ├── SizeDelegate
│ │ └── SizeDelegateViewController.swift
│ ├── Storyboard
│ │ └── StoryboardViewController.swift
│ └── Wheel
│ │ └── WheelViewController.swift
├── ExamplesViewController.swift
└── Resources
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── Header
│ │ ├── Contents.json
│ │ └── Header.imageset
│ │ │ ├── Contents.json
│ │ │ └── Header.jpg
│ ├── Icons
│ │ ├── Contents.json
│ │ ├── axe.imageset
│ │ │ ├── Contents.json
│ │ │ └── axe.pdf
│ │ ├── bonnet.imageset
│ │ │ ├── Contents.json
│ │ │ └── bonnet.pdf
│ │ ├── cloud.imageset
│ │ │ ├── Contents.json
│ │ │ └── cloud.pdf
│ │ ├── compass.imageset
│ │ │ ├── Contents.json
│ │ │ └── compass.pdf
│ │ ├── earth.imageset
│ │ │ ├── Contents.json
│ │ │ └── earth.pdf
│ │ ├── knife.imageset
│ │ │ ├── Contents.json
│ │ │ └── knife.pdf
│ │ ├── leave.imageset
│ │ │ ├── Contents.json
│ │ │ └── leave.pdf
│ │ ├── light.imageset
│ │ │ ├── Contents.json
│ │ │ └── light.pdf
│ │ ├── map.imageset
│ │ │ ├── Contents.json
│ │ │ └── map.pdf
│ │ ├── moon.imageset
│ │ │ ├── Contents.json
│ │ │ └── moon.pdf
│ │ ├── mushroom.imageset
│ │ │ ├── Contents.json
│ │ │ └── mushroom.pdf
│ │ ├── shoes.imageset
│ │ │ ├── Contents.json
│ │ │ └── shoes.pdf
│ │ ├── snow.imageset
│ │ │ ├── Contents.json
│ │ │ └── snow.pdf
│ │ ├── star.imageset
│ │ │ ├── Contents.json
│ │ │ └── star.pdf
│ │ ├── sun.imageset
│ │ │ ├── Contents.json
│ │ │ └── sun.pdf
│ │ ├── tipi.imageset
│ │ │ ├── Contents.json
│ │ │ └── tipi.pdf
│ │ ├── tree.imageset
│ │ │ ├── Contents.json
│ │ │ └── tree.pdf
│ │ ├── water.imageset
│ │ │ ├── Contents.json
│ │ │ └── water.pdf
│ │ ├── wind.imageset
│ │ │ ├── Contents.json
│ │ │ └── wind.pdf
│ │ └── wood.imageset
│ │ │ ├── Contents.json
│ │ │ └── wood.pdf
│ └── Unsplash
│ │ ├── Contents.json
│ │ ├── city-1.imageset
│ │ ├── Contents.json
│ │ └── city-1.jpeg
│ │ ├── city-2.imageset
│ │ ├── Contents.json
│ │ └── city-2.jpeg
│ │ ├── city-3.imageset
│ │ ├── Contents.json
│ │ └── city-3.jpeg
│ │ ├── city-4.imageset
│ │ ├── Contents.json
│ │ └── city-4.jpeg
│ │ ├── coffee-1.imageset
│ │ ├── Contents.json
│ │ └── coffee-1.jpeg
│ │ ├── coffee-2.imageset
│ │ ├── Contents.json
│ │ └── coffee-2.jpeg
│ │ ├── coffee-3.imageset
│ │ ├── Contents.json
│ │ └── coffee-3.jpeg
│ │ ├── coffee-4.imageset
│ │ ├── Contents.json
│ │ └── coffee-4.jpeg
│ │ ├── food-1.imageset
│ │ ├── Contents.json
│ │ └── food-1.jpeg
│ │ ├── food-2.imageset
│ │ ├── Contents.json
│ │ └── food-2.jpeg
│ │ ├── food-3.imageset
│ │ ├── Contents.json
│ │ └── food-3.jpeg
│ │ ├── food-4.imageset
│ │ ├── Contents.json
│ │ └── food-4.jpeg
│ │ ├── green-1.imageset
│ │ ├── Contents.json
│ │ └── green-1.jpeg
│ │ ├── green-2.imageset
│ │ ├── Contents.json
│ │ └── green-2.jpeg
│ │ ├── green-3.imageset
│ │ ├── Contents.json
│ │ └── green-3.jpeg
│ │ ├── green-4.imageset
│ │ ├── Contents.json
│ │ └── green-4.jpeg
│ │ ├── scenic-1.imageset
│ │ ├── Contents.json
│ │ └── scenic-1.jpeg
│ │ ├── scenic-2.imageset
│ │ ├── Contents.json
│ │ └── scenic-2.jpeg
│ │ ├── scenic-3.imageset
│ │ ├── Contents.json
│ │ └── scenic-3.jpeg
│ │ ├── scenic-4.imageset
│ │ ├── Contents.json
│ │ └── scenic-4.jpeg
│ │ ├── succulents-1.imageset
│ │ ├── Contents.json
│ │ └── succulents-1.jpeg
│ │ ├── succulents-2.imageset
│ │ ├── Contents.json
│ │ └── succulents-2.jpeg
│ │ ├── succulents-3.imageset
│ │ ├── Contents.json
│ │ └── succulents-3.jpeg
│ │ └── succulents-4.imageset
│ │ ├── Contents.json
│ │ └── succulents-4.jpeg
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── ContentViewController.swift
│ ├── Info.plist
│ ├── TableViewController.swift
│ ├── UIColor+interpolation.swift
│ └── UIView+constraints.swift
├── ExampleSwiftUI
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ └── LaunchScreen.storyboard
├── ChangeItemsView.swift
├── CustomIndicatorView.swift
├── CustomizedView.swift
├── DefaultView.swift
├── DynamicItemsView.swift
├── ExampleApp.swift
├── Info.plist
├── InterpolatedView.swift
├── LifecycleView.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── ScrollingView.swift
└── SelectedIndexView.swift
├── LICENSE
├── Package.swift
├── Parchment.podspec.json
├── Parchment.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── Example.xcscheme
│ ├── ExampleSwiftUI.xcscheme
│ └── Parchment.xcscheme
├── Parchment
├── Classes
│ ├── PageViewController.swift
│ ├── PageViewCoordinator.swift
│ ├── PageViewManager.swift
│ ├── PagingBorderLayoutAttributes.swift
│ ├── PagingBorderView.swift
│ ├── PagingCell.swift
│ ├── PagingCellLayoutAttributes.swift
│ ├── PagingCollectionViewLayout.swift
│ ├── PagingController.swift
│ ├── PagingFiniteDataSource.swift
│ ├── PagingHostingIndicatorView.swift
│ ├── PagingIndicatorLayoutAttributes.swift
│ ├── PagingIndicatorView.swift
│ ├── PagingInvalidationContext.swift
│ ├── PagingMenuView.swift
│ ├── PagingOptions.swift
│ ├── PagingSizeCache.swift
│ ├── PagingStaticDataSource.swift
│ ├── PagingTitleCell.swift
│ ├── PagingView.swift
│ └── PagingViewController.swift
├── Enums
│ ├── InvalidationState.swift
│ ├── PageViewDirection.swift
│ ├── PageViewState.swift
│ ├── PagingBorderOptions.swift
│ ├── PagingContentInteraction.swift
│ ├── PagingDirection.swift
│ ├── PagingIndicatorOptions.swift
│ ├── PagingMenuHorizontalAlignment.swift
│ ├── PagingMenuInteraction.swift
│ ├── PagingMenuItemSize.swift
│ ├── PagingMenuItemSource.swift
│ ├── PagingMenuPosition.swift
│ ├── PagingMenuTransition.swift
│ ├── PagingSelectedScrollPosition.swift
│ └── PagingState.swift
├── Extensions
│ ├── UIColor+interpolation.swift
│ ├── UIEdgeInsets.swift
│ └── UIView+constraints.swift
├── Parchment.h
├── PrivacyInfo.xcprivacy
├── Protocols
│ ├── CollectionView.swift
│ ├── PageViewControllerDataSource.swift
│ ├── PageViewControllerDelegate.swift
│ ├── PageViewManagerDataSource.swift
│ ├── PageViewManagerDelegate.swift
│ ├── PagingIndicatorStyle.swift
│ ├── PagingItem.swift
│ ├── PagingLayout.swift
│ ├── PagingMenuDataSource.swift
│ ├── PagingMenuDelegate.swift
│ ├── PagingViewControllerDataSource.swift
│ ├── PagingViewControllerDelegate.swift
│ ├── PagingViewControllerInfiniteDataSource.swift
│ ├── PagingViewControllerSizeDelegate.swift
│ └── Tween.swift
├── Resources
│ └── Info.plist
└── Structs
│ ├── AnyPagingItem.swift
│ ├── Page.swift
│ ├── PageContentConfiguration.swift
│ ├── PageItem.swift
│ ├── PageItemBuilder.swift
│ ├── PageItemCell.swift
│ ├── PageState.swift
│ ├── PageView.swift
│ ├── PagingCellViewModel.swift
│ ├── PagingControllerRepresentableView.swift
│ ├── PagingDiff.swift
│ ├── PagingDistance.swift
│ ├── PagingIndexItem.swift
│ ├── PagingIndicatorMetric.swift
│ ├── PagingItems.swift
│ ├── PagingNavigationOrientation.swift
│ └── PagingTransition.swift
├── ParchmentTests
├── Info.plist
├── Item.swift
├── Mocks
│ ├── Mock.swift
│ ├── MockCollectionView.swift
│ ├── MockCollectionViewLayout.swift
│ ├── MockPageViewManagerDataSource.swift
│ ├── MockPageViewManagerDelegate.swift
│ ├── MockPagingControllerDataSource.swift
│ ├── MockPagingControllerDelegate.swift
│ └── MockPagingControllerSizeDelegate.swift
├── PageViewManagerTests.swift
├── PagingCollectionViewLayoutTests.swift
├── PagingControllerTests.swift
├── PagingDataStructureTests.swift
├── PagingDiffTests.swift
├── PagingDistanceCenteredTests.swift
├── PagingDistanceLeftTests.swift
├── PagingDistanceRightTests.swift
├── PagingIndicatorLayoutAttributesTests.swift
├── PagingStateTests.swift
├── PagingViewControllerDelegateTests.swift
├── PagingViewControllerTests.swift
├── PagingViewTests.swift
├── Resources.xcassets
│ ├── Contents.json
│ └── Green.imageset
│ │ ├── Contents.json
│ │ └── Green.png
├── UIColorInterpolationTests.swift
└── Utilities
│ └── CreateDistance.swift
├── ParchmentUITests
├── Info.plist
└── ParchmentUITests.swift
└── README.md
/.github/workflows/parchment.yml:
--------------------------------------------------------------------------------
1 | name: "Parchment"
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: macos-14
6 | steps:
7 | - uses: maxim-lobanov/setup-xcode@v1
8 | with:
9 | xcode-version: '16.0'
10 | - uses: actions/checkout@v3
11 | - name: Unit Tests
12 | run: xcodebuild -project Parchment.xcodeproj -scheme "Parchment" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=18.0' test
13 | - name: UI Tests
14 | run: xcodebuild -project Parchment.xcodeproj -scheme "ParchmentUITests" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=18.0' test
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | build/
4 | .build/
5 | *.pbxuser
6 | !default.pbxuser
7 | *.mode1v3
8 | !default.mode1v3
9 | *.mode2v3
10 | !default.mode2v3
11 | *.perspectivev3
12 | !default.perspectivev3
13 | xcuserdata
14 | *.xccheckout
15 | *.moved-aside
16 | DerivedData
17 | *.hmap
18 | *.ipa
19 | *.xcuserstate
20 |
21 | Carthage/Checkouts
22 | Carthage/Build
23 |
--------------------------------------------------------------------------------
/.images/example-calendar.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/.images/example-calendar.gif
--------------------------------------------------------------------------------
/.images/example-cities.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/.images/example-cities.gif
--------------------------------------------------------------------------------
/.images/example-unsplash.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/.images/example-unsplash.gif
--------------------------------------------------------------------------------
/.images/title-dark-mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/.images/title-dark-mode.png
--------------------------------------------------------------------------------
/.images/title-light-mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/.images/title-light-mode.png
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 6.0
2 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 `PagingIndexItem` 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._
--------------------------------------------------------------------------------
/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @main
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 | var window: UIWindow?
6 | }
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Calendar/DateFormatters.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct DateFormatters {
4 | static let shortDateFormatter: DateFormatter = {
5 | let dateFormatter = DateFormatter()
6 | dateFormatter.timeStyle = .none
7 | dateFormatter.dateStyle = .short
8 | return dateFormatter
9 | }()
10 |
11 | static let dateFormatter: DateFormatter = {
12 | let dateFormatter = DateFormatter()
13 | dateFormatter.dateFormat = "d"
14 | return dateFormatter
15 | }()
16 |
17 | static let weekdayFormatter: DateFormatter = {
18 | let dateFormatter = DateFormatter()
19 | dateFormatter.dateFormat = "EEE"
20 | return dateFormatter
21 | }()
22 | }
23 |
--------------------------------------------------------------------------------
/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/Examples/Icons/IconsViewController.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import UIKit
3 |
4 | struct IconItem: PagingItem, PagingIndexable, Hashable {
5 | let identifier: Int
6 | let icon: String
7 | let index: Int
8 | let image: UIImage?
9 |
10 | init(icon: String, index: Int) {
11 | self.identifier = icon.hashValue
12 | self.icon = icon
13 | self.index = index
14 | self.image = UIImage(named: icon)
15 | }
16 | }
17 |
18 | class IconsViewController: UIViewController {
19 | // Let's start by creating an array of icon names that
20 | // we will use to generate some view controllers.
21 | fileprivate let icons = [
22 | "compass",
23 | "cloud",
24 | "bonnet",
25 | "axe",
26 | "earth",
27 | "knife",
28 | "leave",
29 | "light",
30 | "map",
31 | "moon",
32 | "mushroom",
33 | "shoes",
34 | "snow",
35 | "star",
36 | "sun",
37 | "tipi",
38 | "tree",
39 | "water",
40 | "wind",
41 | "wood",
42 | ]
43 |
44 | override func viewDidLoad() {
45 | super.viewDidLoad()
46 |
47 | let pagingViewController = PagingViewController()
48 | pagingViewController.register(IconPagingCell.self, for: IconItem.self)
49 | pagingViewController.menuItemSize = .fixed(width: 60, height: 60)
50 | pagingViewController.dataSource = self
51 | pagingViewController.select(pagingItem: IconItem(icon: icons[0], index: 0))
52 |
53 | // Add the paging view controller as a child view controller
54 | // and constrain it to all edges.
55 | addChild(pagingViewController)
56 | view.addSubview(pagingViewController.view)
57 | view.constrainToEdges(pagingViewController.view)
58 | pagingViewController.didMove(toParent: self)
59 | }
60 | }
61 |
62 | extension IconsViewController: PagingViewControllerDataSource {
63 | func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController {
64 | return IconViewController(title: icons[index].capitalized)
65 | }
66 |
67 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem {
68 | return IconItem(icon: icons[index], index: index)
69 | }
70 |
71 | func numberOfViewControllers(in _: PagingViewController) -> Int {
72 | return icons.count
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Example/Examples/Images/ImagesViewController.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import UIKit
3 |
4 | @MainActor
5 | protocol ImagesViewControllerDelegate: AnyObject {
6 | func imagesViewControllerDidScroll(_: ImagesViewController)
7 | }
8 |
9 | class ImagesViewController: UIViewController {
10 | weak var delegate: ImagesViewControllerDelegate?
11 |
12 | fileprivate let images: [UIImage]
13 |
14 | fileprivate lazy var collectionViewLayout: UICollectionViewFlowLayout = {
15 | let layout = UICollectionViewFlowLayout()
16 | layout.sectionInset = UIEdgeInsets(top: 18, left: 0, bottom: 18, right: 0)
17 | layout.minimumLineSpacing = 15
18 | return layout
19 | }()
20 |
21 | lazy var collectionView: UICollectionView = {
22 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: self.collectionViewLayout)
23 | collectionView.backgroundColor = .white
24 | return collectionView
25 | }()
26 |
27 | init(images: [UIImage], options _: PagingOptions) {
28 | self.images = images
29 | super.init(nibName: nil, bundle: nil)
30 |
31 | view.addSubview(collectionView)
32 | view.constrainToEdges(collectionView)
33 |
34 | collectionView.dataSource = self
35 | collectionView.delegate = self
36 | collectionView.register(
37 | ImageCollectionViewCell.self,
38 | forCellWithReuseIdentifier: ImageCollectionViewCell.reuseIdentifier
39 | )
40 | }
41 |
42 | required init?(coder _: NSCoder) {
43 | fatalError("init(coder:) has not been implemented")
44 | }
45 |
46 | override func viewWillLayoutSubviews() {
47 | super.viewWillLayoutSubviews()
48 | collectionViewLayout.invalidateLayout()
49 | }
50 | }
51 |
52 | extension ImagesViewController: UICollectionViewDelegateFlowLayout {
53 | func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize {
54 | return CGSize(
55 | width: collectionView.bounds.width - 36,
56 | height: 220
57 | )
58 | }
59 |
60 | func scrollViewDidScroll(_: UIScrollView) {
61 | delegate?.imagesViewControllerDidScroll(self)
62 | }
63 | }
64 |
65 | extension ImagesViewController: UICollectionViewDataSource {
66 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
67 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCollectionViewCell.reuseIdentifier, for: indexPath) as! ImageCollectionViewCell
68 | cell.setImage(images[indexPath.item])
69 | return cell
70 | }
71 |
72 | func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
73 | return images.count
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/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 constrain 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/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 constrain 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 hierarchy 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 |
--------------------------------------------------------------------------------
/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/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/SizeDelegate/SizeDelegateViewController.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import UIKit
3 |
4 | final class SizeDelegateViewController: UIViewController {
5 | // Let's start by creating an array of cities 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 | // constrain 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 | // PagingIndexItem, 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 |
--------------------------------------------------------------------------------
/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 constrain 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 |
--------------------------------------------------------------------------------
/Example/Examples/Wheel/WheelViewController.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import UIKit
3 |
4 | final class WheelViewController: UIViewController {
5 | override func viewDidLoad() {
6 | super.viewDidLoad()
7 |
8 | let viewControllers = [
9 | ContentViewController(index: 0),
10 | ContentViewController(index: 1),
11 | ContentViewController(index: 2),
12 | ContentViewController(index: 3),
13 | ContentViewController(index: 4),
14 | ContentViewController(index: 5),
15 | ContentViewController(index: 6),
16 | ]
17 |
18 | let pagingViewController = PagingViewController(viewControllers: viewControllers)
19 | pagingViewController.menuInteraction = .wheel
20 | pagingViewController.selectedScrollPosition = .center
21 | pagingViewController.menuItemSize = .fixed(width: 100, height: 60)
22 |
23 | addChild(pagingViewController)
24 | view.addSubview(pagingViewController.view)
25 | view.constrainToEdges(pagingViewController.view)
26 | pagingViewController.didMove(toParent: self)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/Header/Header.imageset/Header.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Header/Header.imageset/Header.jpg
--------------------------------------------------------------------------------
/Example/Resources/Assets.xcassets/Icons/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/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/axe.imageset/axe.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/axe.imageset/axe.pdf
--------------------------------------------------------------------------------
/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/bonnet.imageset/bonnet.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/bonnet.imageset/bonnet.pdf
--------------------------------------------------------------------------------
/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/cloud.imageset/cloud.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/cloud.imageset/cloud.pdf
--------------------------------------------------------------------------------
/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/compass.imageset/compass.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/compass.imageset/compass.pdf
--------------------------------------------------------------------------------
/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/earth.imageset/earth.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/earth.imageset/earth.pdf
--------------------------------------------------------------------------------
/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/knife.imageset/knife.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/knife.imageset/knife.pdf
--------------------------------------------------------------------------------
/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/leave.imageset/leave.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/leave.imageset/leave.pdf
--------------------------------------------------------------------------------
/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/light.imageset/light.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/light.imageset/light.pdf
--------------------------------------------------------------------------------
/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/map.imageset/map.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/map.imageset/map.pdf
--------------------------------------------------------------------------------
/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/moon.imageset/moon.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/moon.imageset/moon.pdf
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/Example/Resources/Assets.xcassets/Icons/mushroom.imageset/mushroom.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/mushroom.imageset/mushroom.pdf
--------------------------------------------------------------------------------
/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/shoes.imageset/shoes.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/shoes.imageset/shoes.pdf
--------------------------------------------------------------------------------
/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/snow.imageset/snow.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/snow.imageset/snow.pdf
--------------------------------------------------------------------------------
/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/star.imageset/star.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/star.imageset/star.pdf
--------------------------------------------------------------------------------
/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/sun.imageset/sun.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/sun.imageset/sun.pdf
--------------------------------------------------------------------------------
/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/tipi.imageset/tipi.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/tipi.imageset/tipi.pdf
--------------------------------------------------------------------------------
/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/tree.imageset/tree.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/tree.imageset/tree.pdf
--------------------------------------------------------------------------------
/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/water.imageset/water.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/water.imageset/water.pdf
--------------------------------------------------------------------------------
/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/wind.imageset/wind.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/wind.imageset/wind.pdf
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/Example/Resources/Assets.xcassets/Icons/wood.imageset/wood.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Icons/wood.imageset/wood.pdf
--------------------------------------------------------------------------------
/Example/Resources/Assets.xcassets/Unsplash/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/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-1.imageset/city-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/city-1.imageset/city-1.jpeg
--------------------------------------------------------------------------------
/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-2.imageset/city-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/city-2.imageset/city-2.jpeg
--------------------------------------------------------------------------------
/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-3.imageset/city-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/city-3.imageset/city-3.jpeg
--------------------------------------------------------------------------------
/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/city-4.imageset/city-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/city-4.imageset/city-4.jpeg
--------------------------------------------------------------------------------
/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-1.imageset/coffee-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/coffee-1.imageset/coffee-1.jpeg
--------------------------------------------------------------------------------
/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-2.imageset/coffee-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/coffee-2.imageset/coffee-2.jpeg
--------------------------------------------------------------------------------
/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-3.imageset/coffee-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/coffee-3.imageset/coffee-3.jpeg
--------------------------------------------------------------------------------
/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/coffee-4.imageset/coffee-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/coffee-4.imageset/coffee-4.jpeg
--------------------------------------------------------------------------------
/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-1.imageset/food-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/food-1.imageset/food-1.jpeg
--------------------------------------------------------------------------------
/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-2.imageset/food-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/food-2.imageset/food-2.jpeg
--------------------------------------------------------------------------------
/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-3.imageset/food-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/food-3.imageset/food-3.jpeg
--------------------------------------------------------------------------------
/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/food-4.imageset/food-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/food-4.imageset/food-4.jpeg
--------------------------------------------------------------------------------
/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-1.imageset/green-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/green-1.imageset/green-1.jpeg
--------------------------------------------------------------------------------
/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-2.imageset/green-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/green-2.imageset/green-2.jpeg
--------------------------------------------------------------------------------
/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-3.imageset/green-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/green-3.imageset/green-3.jpeg
--------------------------------------------------------------------------------
/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/green-4.imageset/green-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/green-4.imageset/green-4.jpeg
--------------------------------------------------------------------------------
/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-1.imageset/scenic-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/scenic-1.imageset/scenic-1.jpeg
--------------------------------------------------------------------------------
/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-2.imageset/scenic-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/scenic-2.imageset/scenic-2.jpeg
--------------------------------------------------------------------------------
/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-3.imageset/scenic-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/scenic-3.imageset/scenic-3.jpeg
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/Example/Resources/Assets.xcassets/Unsplash/scenic-4.imageset/scenic-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/scenic-4.imageset/scenic-4.jpeg
--------------------------------------------------------------------------------
/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-1.imageset/succulents-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/succulents-1.imageset/succulents-1.jpeg
--------------------------------------------------------------------------------
/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-2.imageset/succulents-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/succulents-2.imageset/succulents-2.jpeg
--------------------------------------------------------------------------------
/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-3.imageset/succulents-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/succulents-3.imageset/succulents-3.jpeg
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/Example/Resources/Assets.xcassets/Unsplash/succulents-4.imageset/succulents-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/Example/Resources/Assets.xcassets/Unsplash/succulents-4.imageset/succulents-4.jpeg
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | UIViewControllerBasedStatusBarAppearance
47 |
48 | UIStatusBarStyle
49 | UIStatusBarStyleLightContent
50 |
51 |
52 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Example/Resources/UIView+constraints.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIView {
4 | func constrainCentered(_ subview: UIView) {
5 | subview.translatesAutoresizingMaskIntoConstraints = false
6 |
7 | let verticalConstraint = 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 horizontalConstraint = 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 heightConstraint = 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 widthConstraint = 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 | horizontalConstraint,
49 | verticalConstraint,
50 | heightConstraint,
51 | widthConstraint,
52 | ])
53 | }
54 |
55 | func constrainToEdges(_ subview: UIView) {
56 | subview.translatesAutoresizingMaskIntoConstraints = false
57 |
58 | NSLayoutConstraint.activate([
59 | subview.leadingAnchor.constraint(equalTo: leadingAnchor),
60 | subview.trailingAnchor.constraint(equalTo: trailingAnchor),
61 | subview.bottomAnchor.constraint(equalTo: bottomAnchor),
62 | subview.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor)
63 | ])
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/ExampleSwiftUI/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/ChangeItemsView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct ChangeItemsView: View {
6 | @State var isToggled: Bool = false
7 |
8 | var body: some View {
9 | PageView {
10 | if isToggled {
11 | Page("Title 2") {
12 | VStack {
13 | Text("Page 2")
14 | .font(.largeTitle)
15 | .padding(.bottom)
16 |
17 | Button("Click me") {
18 | isToggled.toggle()
19 | }
20 | }
21 | }
22 |
23 | Page("Title 3") {
24 | VStack {
25 | Text("Page 3")
26 | .font(.largeTitle)
27 | .padding(.bottom)
28 |
29 | Button("Click me") {
30 | isToggled.toggle()
31 | }
32 | }
33 | }
34 | } else {
35 | Page("Title 0") {
36 | VStack {
37 | Text("Page 0")
38 | .font(.largeTitle)
39 | .padding(.bottom)
40 |
41 | Button("Click me") {
42 | isToggled.toggle()
43 | }
44 | }
45 | }
46 |
47 | Page("Title 1") {
48 | VStack {
49 | Text("Page 1")
50 | .font(.largeTitle)
51 | .padding(.bottom)
52 |
53 | Button("Click me") {
54 | isToggled.toggle()
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/CustomIndicatorView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct CustomIndicatorView: View {
6 | var body: some View {
7 | PageView {
8 | Page("Scone") {
9 | Text("Scone")
10 | .font(.largeTitle)
11 | .foregroundColor(.gray)
12 | }
13 |
14 | Page("Cinnamon Roll") {
15 | Text("Cinnamon Roll")
16 | .font(.largeTitle)
17 | .foregroundColor(.gray)
18 | }
19 |
20 | Page("Croissant") {
21 | Text("Croissant")
22 | .font(.largeTitle)
23 | .foregroundColor(.gray)
24 | }
25 |
26 | Page("Muffin") {
27 | Text("Muffin")
28 | .font(.largeTitle)
29 | .foregroundColor(.gray)
30 | }
31 | }
32 | .borderColor(.black.opacity(0.1))
33 | .indicatorOptions(.visible(height: 2))
34 | .indicatorStyle(SquigglyIndicatorStyle())
35 |
36 | }
37 | }
38 |
39 | struct SquigglyIndicatorStyle: PagingIndicatorStyle {
40 | func makeBody(configuration: Configuration) -> some View {
41 | SquigglyShape()
42 | .stroke(.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round))
43 | }
44 | }
45 |
46 | struct SquigglyShape: Shape {
47 | func path(in rect: CGRect) -> Path {
48 | var path = Path()
49 | path.move(to: .zero)
50 |
51 | for x in stride(from: 0, through: rect.width, by: 1) {
52 | let sine = sin(x / 1.5)
53 | let y = rect.height * sine
54 | path.addLine(to: CGPoint(x: x, y: y))
55 | }
56 |
57 | return path
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/CustomizedView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct CustomizedView: View {
6 | var body: some View {
7 | PageView {
8 | Page("Title 1") {
9 | VStack(spacing: 25) {
10 | Text("Page 1")
11 | Image(systemName: "arrow.down")
12 | }
13 | .font(.largeTitle)
14 | }
15 |
16 | Page("Title 2") {
17 | VStack(spacing: 25) {
18 | Image(systemName: "arrow.up")
19 | Text("Page 2")
20 | }
21 | .font(.largeTitle)
22 | }
23 | }
24 | .menuItemSize(.fixed(width: 100, height: 60))
25 | .menuItemSpacing(20)
26 | .menuItemLabelSpacing(30)
27 | .selectedColor(.blue)
28 | .foregroundColor(.black)
29 | .menuBackgroundColor(.white)
30 | .backgroundColor(.white)
31 | .selectedBackgroundColor(.white)
32 | .menuInsets(.vertical, 20)
33 | .menuHorizontalAlignment(.center)
34 | .menuPosition(.bottom)
35 | .menuTransition(.scrollAlongside)
36 | .menuInteraction(.swipe)
37 | .contentInteraction(.scrolling)
38 | .contentNavigationOrientation(.vertical)
39 | .selectedScrollPosition(.preferCentered)
40 | .indicatorOptions(.visible(height: 4))
41 | .indicatorColor(.blue)
42 | .borderOptions(.visible(height: 4))
43 | .borderColor(.blue.opacity(0.2))
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/DefaultView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct DefaultView: View {
6 | var body: some View {
7 | PageView {
8 | Page { _ in
9 | Image(systemName: "star.fill")
10 | .padding()
11 | } content: {
12 | Text("Page 1")
13 | .font(.largeTitle)
14 | .foregroundColor(.gray)
15 | }
16 |
17 | Page("Title 2") {
18 | Text("Page 2")
19 | .font(.largeTitle)
20 | .foregroundColor(.gray)
21 | }
22 |
23 | Page("Title 3") {
24 | Text("Page 3")
25 | .font(.largeTitle)
26 | .foregroundColor(.gray)
27 | }
28 |
29 | Page("Title 4") {
30 | Text("Page 4")
31 | .font(.largeTitle)
32 | .foregroundColor(.gray)
33 | }
34 |
35 | Page("Some very long title") {
36 | Text("Page 5")
37 | .font(.largeTitle)
38 | .foregroundColor(.gray)
39 | }
40 |
41 | Page("Title 6") {
42 | Text("Page 6")
43 | .font(.largeTitle)
44 | .foregroundColor(.gray)
45 | }
46 |
47 | Page("Title 7") {
48 | Text("Page 7")
49 | .font(.largeTitle)
50 | .foregroundColor(.gray)
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/DynamicItemsView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct DynamicItemsView: View {
6 | @State var items: [Int] = [0, 1, 2, 3, 4]
7 |
8 | var body: some View {
9 | PageView(items, id: \.self) { item in
10 | Page("Title \(item)") {
11 | VStack {
12 | Text("Page \(item)")
13 | .font(.largeTitle)
14 | .padding(.bottom)
15 |
16 | Button("Click me") {
17 | if items.count > 2 {
18 | items = [5, 6]
19 | } else {
20 | items = [0, 1, 2, 3, 4]
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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 | Text("**Welcome to Parchment**. These examples shows how to use Parchment with SwiftUI. For more advanced examples, see the UIKit examples or reach out on GitHub Discussions.")
10 |
11 | Section {
12 | NavigationLink("Default", destination: DefaultView())
13 | NavigationLink("Interpolated", destination: InterpolatedView())
14 | NavigationLink("Customized", destination: CustomizedView())
15 | NavigationLink("Change selected index", destination: SelectedIndexView())
16 | NavigationLink("Lifecycle events", destination: LifecycleView())
17 | NavigationLink("Change items", destination: ChangeItemsView())
18 | NavigationLink("Dynamic items", destination: DynamicItemsView())
19 | NavigationLink("Custom indicator", destination: CustomIndicatorView())
20 | NavigationLink("Scrolling Views", destination: ScrollingView())
21 | }
22 | }
23 | .navigationBarTitleDisplayMode(.inline)
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/InterpolatedView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct InterpolatedView: View {
6 | var body: some View {
7 | PageView {
8 | Page { state in
9 | Image(systemName: "star.fill")
10 | .scaleEffect(x: 1 + state.progress, y: 1 + state.progress)
11 | .rotationEffect(Angle(degrees: 180 * state.progress))
12 | .padding(30 * state.progress + 20)
13 | } content: {
14 | Text("Page 1")
15 | .font(.largeTitle)
16 | .foregroundColor(.secondary)
17 | }
18 | Page { state in
19 | Text("Rotate")
20 | .fixedSize()
21 | .rotationEffect(Angle(degrees: 90 * state.progress))
22 | .padding(.horizontal, 10)
23 | } content: {
24 | Text("Page 2")
25 | .font(.largeTitle)
26 | .foregroundColor(.secondary)
27 | }
28 |
29 | Page { state in
30 | Text("Tracking")
31 | .tracking(10 * state.progress)
32 | .fixedSize()
33 | .padding()
34 | } content: {
35 | Text("Page 3")
36 | .font(.largeTitle)
37 | .foregroundColor(.secondary)
38 | }
39 |
40 | Page { state in
41 | Text("Growing")
42 | .fixedSize()
43 | .padding(.vertical)
44 | .padding(.horizontal, 20 * state.progress + 10)
45 | .background(Color.black.opacity(0.1))
46 | .cornerRadius(6)
47 | } content: {
48 | Text("Page 4")
49 | .font(.largeTitle)
50 | .foregroundColor(.secondary)
51 | }
52 |
53 | Page("Normal") {
54 | Text("Page 5")
55 | .font(.largeTitle)
56 | .foregroundColor(.secondary)
57 | }
58 |
59 | Page("Normal") {
60 | Text("Page 6")
61 | .font(.largeTitle)
62 | .foregroundColor(.secondary)
63 | }
64 | }
65 | .menuItemSize(.selfSizing(estimatedWidth: 100, height: 80))
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/LifecycleView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct LifecycleView: View {
6 | var body: some View {
7 | PageView {
8 | Page("Title 1") {
9 | Text("Page 1")
10 | .font(.largeTitle)
11 | .foregroundColor(.gray)
12 | }
13 |
14 | Page("Title 2") {
15 | Text("Page 2")
16 | .font(.largeTitle)
17 | .foregroundColor(.gray)
18 | }
19 |
20 | Page("Title 3") {
21 | Text("Page 3")
22 | .font(.largeTitle)
23 | .foregroundColor(.gray)
24 | }
25 | }
26 | .willScroll { item in
27 | print("will scroll: ", item)
28 | }
29 | .didScroll { item in
30 | print("did scroll: ", item)
31 | }
32 | .didSelect { item in
33 | print("did select: ", item)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/ExampleSwiftUI/ScrollingView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct ScrollingView: View {
6 | var body: some View {
7 | PageView {
8 | Page("First") {
9 | ScrollingContentView()
10 | }
11 | Page("Second") {
12 | ScrollingContentView()
13 | }
14 | Page("Third") {
15 | ScrollingContentView()
16 | }
17 | }
18 | }
19 | }
20 |
21 | struct ScrollingContentView: View {
22 | var body: some View {
23 | List {
24 | ForEach(0...50 , id: \.self) { item in
25 | NavigationLink(destination: Text("\(item)")) {
26 | Text("\(item)")
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/ExampleSwiftUI/SelectedIndexView.swift:
--------------------------------------------------------------------------------
1 | import Parchment
2 | import SwiftUI
3 | import UIKit
4 |
5 | struct SelectedIndexView: View {
6 | @State var selectedIndex: Int = 0
7 |
8 | var body: some View {
9 | PageView(selectedIndex: $selectedIndex) {
10 | Page("Title 0") {
11 | VStack {
12 | Text("Page 0")
13 | .font(.largeTitle)
14 | .padding(.bottom)
15 |
16 | Button("Click me") {
17 | selectedIndex = 2
18 | }
19 | }
20 | }
21 |
22 | Page("Title 1") {
23 | Text("Page 1")
24 | .font(.largeTitle)
25 | .foregroundColor(.gray)
26 | }
27 |
28 | Page("Title 2") {
29 | VStack {
30 | Text("Page 2")
31 | .font(.largeTitle)
32 | .padding(.bottom)
33 |
34 | Button("Click me") {
35 | selectedIndex = 0
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Parchment",
6 | platforms: [.iOS(.v12)],
7 | products: [
8 | .library(name: "Parchment", targets: ["Parchment"]),
9 | ],
10 | targets: [
11 | .target(
12 | name: "Parchment",
13 | path: "Parchment",
14 | resources: [
15 | .copy("PrivacyInfo.xcprivacy")
16 | ]
17 | )
18 | ]
19 | )
20 |
--------------------------------------------------------------------------------
/Parchment.podspec.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Parchment",
3 | "version": "4.1.0",
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": "Martin Rechsteiner",
9 | "source": {
10 | "git": "https://github.com/rechsteiner/Parchment.git",
11 | "tag": "v4.1.0"
12 | },
13 | "swift_version": "6.0",
14 | "platforms": {
15 | "ios": "12.0"
16 | },
17 | "source_files": [
18 | "Parchment/**/*.swift",
19 | "Parchment/*.swift"
20 | ],
21 | "weak_frameworks": [
22 | "SwiftUI"
23 | ],
24 | "requires_arc": true
25 | }
26 |
--------------------------------------------------------------------------------
/Parchment.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Parchment.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Parchment.xcodeproj/xcshareddata/xcschemes/Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Parchment.xcodeproj/xcshareddata/xcschemes/ExampleSwiftUI.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Parchment/Classes/PageViewCoordinator.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @available(iOS 14.0, *)
4 | @MainActor
5 | final class PageViewCoordinator: PagingViewControllerDataSource, PagingViewControllerDelegate {
6 | final class WeakReference {
7 | weak var value: T?
8 |
9 | init(value: T) {
10 | self.value = value
11 | }
12 | }
13 |
14 | var parent: PagingControllerRepresentableView
15 | var controllers: [Int: WeakReference] = [:]
16 |
17 | init(_ pagingController: PagingControllerRepresentableView) {
18 | parent = pagingController
19 | }
20 |
21 | func numberOfViewControllers(in _: PagingViewController) -> Int {
22 | return parent.items.count
23 | }
24 |
25 | func pagingViewController(
26 | _: PagingViewController,
27 | viewControllerAt index: Int
28 | ) -> UIViewController {
29 | let item = parent.items[index]
30 | let hostingViewController: UIViewController
31 |
32 | if let controller = controllers[item.identifier]?.value {
33 | hostingViewController = controller
34 | } else {
35 | let controller = hostingController(for: item)
36 | controllers[item.identifier] = WeakReference(value: controller)
37 | hostingViewController = controller
38 | }
39 |
40 | let backgroundColor = parent.options.pagingContentBackgroundColor
41 | hostingViewController.view.backgroundColor = backgroundColor
42 | return hostingViewController
43 | }
44 |
45 | func pagingViewController(
46 | _: PagingViewController,
47 | pagingItemAt index: Int
48 | ) -> PagingItem {
49 | parent.items[index]
50 | }
51 |
52 | func pagingViewController(
53 | _ controller: PagingViewController,
54 | didScrollToItem pagingItem: PagingItem,
55 | startingViewController _: UIViewController?,
56 | destinationViewController _: UIViewController,
57 | transitionSuccessful _: Bool
58 | ) {
59 | if let item = pagingItem as? PageItem,
60 | let index = parent.items.firstIndex(where: { $0.isEqual(to: item) }) {
61 | parent.selectedIndex = index
62 | }
63 |
64 | parent.onDidScroll?(pagingItem)
65 | }
66 |
67 | func pagingViewController(
68 | _: PagingViewController,
69 | willScrollToItem pagingItem: PagingItem,
70 | startingViewController _: UIViewController,
71 | destinationViewController _: UIViewController
72 | ) {
73 | parent.onWillScroll?(pagingItem)
74 | }
75 |
76 | func pagingViewController(
77 | _: PagingViewController,
78 | didSelectItem pagingItem: PagingItem
79 | ) {
80 | parent.onDidSelect?(pagingItem)
81 | }
82 |
83 | private func hostingController(for pagingItem: PagingItem) -> UIViewController {
84 | var hostingViewController: UIViewController
85 | if let item = pagingItem as? PageItem {
86 | hostingViewController = item.page.content()
87 | } else {
88 | assertionFailure("""
89 | PageItem is required when using the SwiftUI wrappers.
90 | Please report if you somehow ended up here.
91 | """)
92 | hostingViewController = UIViewController()
93 | }
94 | return hostingViewController
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Parchment/Classes/PagingBorderLayoutAttributes.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | open class PagingBorderLayoutAttributes: UICollectionViewLayoutAttributes {
4 | nonisolated(unsafe) open var backgroundColor: UIColor?
5 | nonisolated(unsafe) 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 |
--------------------------------------------------------------------------------
/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/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/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 | nonisolated(unsafe) 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 |
--------------------------------------------------------------------------------
/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/Classes/PagingHostingIndicatorView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 |
4 | /// A custom `UICollectionViewReusableView` subclass used to display a
5 | /// view that indicates the currently selected cell. You can subclass
6 | /// this type if you need further customization; just override the
7 | /// `indicatorClass` property in `PagingViewController`.
8 | @available(iOS 14.0, *)
9 | final class PagingHostingIndicatorView: PagingIndicatorView {
10 | private let hostingController: UIHostingController
11 |
12 | override init(frame: CGRect) {
13 | let configuration = PagingIndicatorConfiguration(backgroundColor: .clear)
14 | let rootView = PagingIndicator(configuration: configuration)
15 | self.hostingController = UIHostingController(rootView: rootView)
16 |
17 | super.init(frame: frame)
18 |
19 | hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
20 | hostingController.view.backgroundColor = .clear
21 | hostingController.view.clipsToBounds = false
22 | clipsToBounds = false
23 | }
24 |
25 | required init?(coder: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | override func didMoveToWindow() {
30 | super.didMoveToWindow()
31 | if window == nil {
32 | hostingController.willMove(toParent: nil)
33 | hostingController.removeFromParent()
34 | hostingController.didMove(toParent: nil)
35 | } else if let parent = parentViewController() {
36 | hostingController.willMove(toParent: parent)
37 | parent.addChild(hostingController)
38 | addSubview(hostingController.view)
39 | hostingController.didMove(toParent: parent)
40 | }
41 | }
42 |
43 | public override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
44 | if let attributes = layoutAttributes as? PagingIndicatorLayoutAttributes {
45 | let configuration = PagingIndicatorConfiguration(
46 | backgroundColor: Color(attributes.backgroundColor ?? .clear)
47 | )
48 | hostingController.rootView = PagingIndicator(configuration: configuration)
49 | hostingController.view.frame = bounds
50 | }
51 | }
52 |
53 | private func parentViewController() -> UIViewController? {
54 | var responder: UIResponder? = self
55 | while let nextResponder = responder?.next {
56 | if let viewController = nextResponder as? UIViewController {
57 | return viewController
58 | }
59 | responder = nextResponder
60 | }
61 | return nil
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Parchment/Classes/PagingIndicatorLayoutAttributes.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | open class PagingIndicatorLayoutAttributes: UICollectionViewLayoutAttributes {
4 | nonisolated(unsafe) 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/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 |
--------------------------------------------------------------------------------
/Parchment/Classes/PagingInvalidationContext.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | open class PagingInvalidationContext: UICollectionViewLayoutInvalidationContext {
4 | var invalidateSizes: Bool = false
5 | }
6 |
--------------------------------------------------------------------------------
/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 | NotificationCenter.default.addObserver(
16 | self,
17 | selector: #selector(didReceiveMemoryWarning(notification:)),
18 | name: UIApplication.didReceiveMemoryWarningNotification,
19 | object: nil
20 | )
21 | }
22 |
23 | deinit {
24 | NotificationCenter.default.removeObserver(self)
25 | }
26 |
27 | func clear() {
28 | sizeCache = [:]
29 | selectedSizeCache = [:]
30 | }
31 |
32 | func itemSize(for pagingItem: PagingItem) -> CGFloat {
33 | if let size = sizeCache[pagingItem.identifier] {
34 | return size
35 | } else {
36 | let size = sizeForPagingItem?(pagingItem, false)
37 | sizeCache[pagingItem.identifier] = size
38 | return size ?? options.estimatedItemWidth
39 | }
40 | }
41 |
42 | func itemWidthSelected(for pagingItem: PagingItem) -> CGFloat {
43 | if let size = selectedSizeCache[pagingItem.identifier] {
44 | return size
45 | } else {
46 | let size = sizeForPagingItem?(pagingItem, true)
47 | selectedSizeCache[pagingItem.identifier] = size
48 | return size ?? options.estimatedItemWidth
49 | }
50 | }
51 |
52 | @objc private func didReceiveMemoryWarning(notification _: NSNotification) {
53 | clear()
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Parchment/Classes/PagingStaticDataSource.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | @MainActor
5 | final class PagingStaticDataSource: PagingViewControllerInfiniteDataSource {
6 | private(set) var items: [PagingItem] = []
7 | private let viewControllers: [UIViewController]
8 |
9 | init(viewControllers: [UIViewController]) {
10 | self.viewControllers = viewControllers
11 | reloadItems()
12 | }
13 |
14 | func pagingItem(at index: Int) -> PagingItem {
15 | return items[index]
16 | }
17 |
18 | func reloadItems() {
19 | items = viewControllers.enumerated().map {
20 | PagingIndexItem(index: $0, title: $1.title ?? "")
21 | }
22 | }
23 |
24 | func pagingViewController(_: PagingViewController, viewControllerFor pagingItem: PagingItem) -> UIViewController {
25 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else {
26 | fatalError("pagingViewController:viewControllerFor: PagingItem does not exist")
27 | }
28 | return viewControllers[index]
29 | }
30 |
31 | func pagingViewController(_: PagingViewController, itemBefore pagingItem: PagingItem) -> PagingItem? {
32 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { return nil }
33 | if index > 0 {
34 | return items[index - 1]
35 | }
36 | return nil
37 | }
38 |
39 | func pagingViewController(_: PagingViewController, itemAfter pagingItem: PagingItem) -> PagingItem? {
40 | guard let index = items.firstIndex(where: { $0.isEqual(to: pagingItem) }) else { return nil }
41 | if index < items.count - 1 {
42 | return items[index + 1]
43 | }
44 | return nil
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/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.menuHeight
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 | /// Configures the view hierarchy, sets up the layout constraints
45 | /// and does any other customization based on the `PagingOptions`.
46 | /// Override this if you need any custom behavior.
47 | open func configure() {
48 | collectionView.backgroundColor = options.menuBackgroundColor
49 | addSubview(pageView)
50 | addSubview(collectionView)
51 | setupConstraints()
52 | }
53 |
54 | /// Sets up all the layout constraints. Override this if you need to
55 | /// make changes to how the views are layed out.
56 | open func setupConstraints() {
57 | collectionView.translatesAutoresizingMaskIntoConstraints = false
58 | pageView.translatesAutoresizingMaskIntoConstraints = false
59 |
60 | let heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: options.menuHeight)
61 | heightConstraint.isActive = true
62 | heightConstraint.priority = .defaultHigh
63 | self.heightConstraint = heightConstraint
64 |
65 | NSLayoutConstraint.activate([
66 | collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
67 | collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
68 | pageView.leadingAnchor.constraint(equalTo: leadingAnchor),
69 | pageView.trailingAnchor.constraint(equalTo: trailingAnchor)
70 | ])
71 |
72 | switch options.menuPosition {
73 | case .top:
74 | NSLayoutConstraint.activate([
75 | collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
76 | pageView.topAnchor.constraint(equalTo: collectionView.bottomAnchor),
77 | pageView.bottomAnchor.constraint(equalTo: bottomAnchor),
78 | ])
79 | case .bottom:
80 | NSLayoutConstraint.activate([
81 | pageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
82 | pageView.bottomAnchor.constraint(equalTo: collectionView.topAnchor),
83 | collectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
84 | ])
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/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 | @MainActor
10 | public enum InvalidationState {
11 | case nothing
12 | case everything
13 | case sizes
14 |
15 | public init(_ invalidationContext: UICollectionViewLayoutInvalidationContext) {
16 | if invalidationContext.invalidateEverything {
17 | self = .everything
18 | } else if invalidationContext.invalidateDataSourceCounts {
19 | self = .everything
20 | } else if let context = invalidationContext as? PagingInvalidationContext {
21 | if context.invalidateSizes {
22 | self = .sizes
23 | } else {
24 | self = .nothing
25 | }
26 | } else {
27 | self = .nothing
28 | }
29 | }
30 |
31 | public static func + (lhs: InvalidationState, rhs: InvalidationState) -> InvalidationState {
32 | switch (lhs, rhs) {
33 | case (.everything, _), (_, .everything):
34 | return .everything
35 | case (.sizes, _), (_, .sizes):
36 | return .sizes
37 | case (.nothing, _), (_, .nothing):
38 | return .nothing
39 | default:
40 | return .everything
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/PagingBorderOptions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public enum PagingBorderOptions {
4 | case hidden
5 | case visible(
6 | height: CGFloat,
7 | zIndex: Int = 0,
8 | insets: UIEdgeInsets = .zero
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/Parchment/Enums/PagingContentInteraction.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum PagingContentInteraction {
4 | case scrolling
5 | case none
6 | }
7 |
--------------------------------------------------------------------------------
/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/Enums/PagingIndicatorOptions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public enum PagingIndicatorOptions {
4 | case hidden
5 | case visible(
6 | height: CGFloat,
7 | zIndex: Int = 1,
8 | spacing: UIEdgeInsets = .zero,
9 | insets: UIEdgeInsets = .zero
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/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 | // Allows all paging items to be right centered within the paging menu
11 | // when PagingMenuItemSize is .fixed and the sum of the widths
12 | // of all the paging items are less than the paging menu
13 | case right
14 | }
15 |
--------------------------------------------------------------------------------
/Parchment/Enums/PagingMenuInteraction.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum PagingMenuInteraction {
4 | case scrolling
5 | case swipe
6 | case wheel
7 | case none
8 | }
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Parchment/Enums/PagingMenuPosition.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum PagingMenuPosition {
4 | case top // default
5 | case bottom
6 | }
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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. Effectively the same as .centeredHorizontally on
11 | /// UICollectionViewScrollPosition.
12 | case preferCentered
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Extensions/UIView+constraints.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIView {
4 | func constrainToEdges(_ subview: UIView) {
5 | subview.translatesAutoresizingMaskIntoConstraints = false
6 |
7 | let topConstraint = 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 leadingConstraint = 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 trailingConstraint = 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 | topConstraint,
49 | bottomConstraint,
50 | leadingConstraint,
51 | trailingConstraint,
52 | ])
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Parchment/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyCollectedDataTypes
6 |
7 |
8 | NSPrivacyCollectedDataType
9 |
10 | NSPrivacyCollectedDataTypeLinked
11 |
12 | NSPrivacyCollectedDataTypeTracking
13 |
14 | NSPrivacyCollectedDataTypePurposes
15 |
16 |
17 |
18 |
19 |
20 | NSPrivacyTracking
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Parchment/Protocols/CollectionView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @MainActor
4 | protocol CollectionViewLayout: AnyObject {
5 | var state: PagingState { get set }
6 | var visibleItems: PagingItems { get set }
7 | var sizeCache: PagingSizeCache? { get set }
8 | var contentInsets: UIEdgeInsets { get }
9 | var layoutAttributes: [IndexPath: PagingCellLayoutAttributes] { get }
10 | func prepare()
11 | func invalidateLayout()
12 | func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext)
13 | }
14 |
15 | extension PagingCollectionViewLayout: CollectionViewLayout {}
16 |
17 | @MainActor
18 | protocol CollectionView: AnyObject {
19 | var indexPathsForVisibleItems: [IndexPath] { get }
20 | var isDragging: Bool { get }
21 | var window: UIWindow? { get }
22 | var superview: UIView? { get }
23 | var bounds: CGRect { get }
24 | var contentOffset: CGPoint { get set }
25 | var contentSize: CGSize { get }
26 | var contentInset: UIEdgeInsets { get }
27 | var showsHorizontalScrollIndicator: Bool { get set }
28 | var dataSource: UICollectionViewDataSource? { get set }
29 | var isScrollEnabled: Bool { get set }
30 | var alwaysBounceHorizontal: Bool { get set }
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 | func indexPathForItem(at point: CGPoint) -> IndexPath?
42 | }
43 |
44 | extension UICollectionView: CollectionView {}
45 |
46 | enum Edge {
47 | case left, right, top, bottom
48 | }
49 |
50 | extension CollectionView {
51 | func near(edge: Edge, clearance: CGFloat = 0) -> Bool {
52 | switch edge {
53 | case .left:
54 | return contentOffset.x + contentInset.left - clearance <= 0
55 | case .right:
56 | return (contentOffset.x + bounds.width + clearance) >= contentSize.width
57 | case .top:
58 | return contentOffset.y + contentInset.top - clearance <= 0
59 | case .bottom:
60 | return (contentOffset.y + bounds.height + clearance) >= contentSize.height
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/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 | @MainActor
12 | func pageViewController(
13 | _ pageViewController: PageViewController,
14 | viewControllerBeforeViewController viewController: UIViewController
15 | ) -> UIViewController?
16 |
17 | /// Return the view controller after a given view controller.
18 | ///
19 | /// - Parameters:
20 | /// - pageViewController: The `PageViewController` instance.
21 | /// - viewController: The current view controller.
22 | @MainActor
23 | func pageViewController(
24 | _ pageViewController: PageViewController,
25 | viewControllerAfterViewController viewController: UIViewController
26 | ) -> UIViewController?
27 | }
28 |
--------------------------------------------------------------------------------
/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 | @MainActor
17 | func pageViewController(
18 | _ pageViewController: PageViewController,
19 | willStartScrollingFrom startingViewController: UIViewController,
20 | destinationViewController: UIViewController
21 | )
22 |
23 | /// Called whenever a scroll transition is in progress.
24 | ///
25 | /// - Parameters:
26 | /// - pageViewController: The `PageViewController` instance.
27 | /// - startingViewController: The view controller the user is
28 | /// scrolling from.
29 | /// - destinationViewController: The view controller the user is
30 | /// scrolling towards. Will be nil if the user is scrolling
31 | /// towards one of the edges.
32 | /// - progress: The progress of the scroll transition. Between 0
33 | /// and 1.
34 | @MainActor
35 | func pageViewController(
36 | _ pageViewController: PageViewController,
37 | isScrollingFrom startingViewController: UIViewController,
38 | destinationViewController: UIViewController?,
39 | progress: CGFloat
40 | )
41 |
42 | /// Called when the user finished scrolling to a new view.
43 | ///
44 | /// - Parameters:
45 | /// - pageViewController: The `PageViewController` instance.
46 | /// - startingViewController: The view controller the user is
47 | /// scrolling from.
48 | /// - destinationViewController: The view controller the user is
49 | /// scrolling towards.
50 | /// - transitionSuccessful: A boolean indicating whether the
51 | /// transition completed, or was cancelled by the user.
52 | @MainActor
53 | func pageViewController(
54 | _ pageViewController: PageViewController,
55 | didFinishScrollingFrom startingViewController: UIViewController,
56 | destinationViewController: UIViewController,
57 | transitionSuccessful: Bool
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/Parchment/Protocols/PageViewManagerDataSource.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @MainActor
4 | protocol PageViewManagerDataSource: AnyObject {
5 | func viewControllerBefore(_ viewController: UIViewController) -> UIViewController?
6 | func viewControllerAfter(_ viewController: UIViewController) -> UIViewController?
7 | }
8 |
--------------------------------------------------------------------------------
/Parchment/Protocols/PageViewManagerDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @MainActor
4 | protocol PageViewManagerDelegate: AnyObject {
5 | func scrollForward()
6 | func scrollReverse()
7 | func layoutViews(for viewControllers: [UIViewController], keepContentOffset: Bool)
8 | func addViewController(_ viewController: UIViewController)
9 | func removeViewController(_ viewController: UIViewController)
10 | func beginAppearanceTransition(
11 | isAppearing: Bool,
12 | viewController: UIViewController,
13 | animated: Bool
14 | )
15 | func endAppearanceTransition(viewController: UIViewController)
16 | func willScroll(
17 | from selectedViewController: UIViewController,
18 | to destinationViewController: UIViewController
19 | )
20 | func isScrolling(
21 | from selectedViewController: UIViewController,
22 | to destinationViewController: UIViewController?,
23 | progress: CGFloat
24 | )
25 | func didFinishScrolling(
26 | from selectedViewController: UIViewController,
27 | to destinationViewController: UIViewController,
28 | transitionSuccessful: Bool
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/Parchment/Protocols/PagingIndicatorStyle.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | @available(iOS 14.0, *)
5 | public protocol PagingIndicatorStyle: Sendable {
6 | associatedtype Body: View
7 | typealias Configuration = PagingIndicatorConfiguration
8 | @ViewBuilder func makeBody(configuration: Configuration) -> Body
9 | }
10 |
11 | @available(iOS 14.0, *)
12 | struct PagingIndicator: View {
13 | let configuration: PagingIndicatorConfiguration
14 |
15 | @Environment(\.indicatorStyle) var style
16 |
17 | var body: some View {
18 | AnyView(style.makeBody(configuration: configuration))
19 | }
20 | }
21 |
22 | @available(iOS 14.0, *)
23 | public struct PagingIndicatorConfiguration {
24 | public let backgroundColor: Color
25 | }
26 |
27 | @available(iOS 14.0, *)
28 | struct DefaultPagingIndicatorStyle: PagingIndicatorStyle {
29 | func makeBody(configuration: Configuration) -> some View {
30 | Rectangle()
31 | .fill(configuration.backgroundColor)
32 | }
33 | }
34 |
35 | @available(iOS 14.0, *)
36 | struct PagingIndicatorStyleKey: EnvironmentKey {
37 | static let defaultValue: any PagingIndicatorStyle = DefaultPagingIndicatorStyle()
38 | }
39 |
40 | @available(iOS 14.0, *)
41 | extension EnvironmentValues {
42 | var indicatorStyle: any PagingIndicatorStyle {
43 | get { self[PagingIndicatorStyleKey.self] }
44 | set { self[PagingIndicatorStyleKey.self] = newValue }
45 | }
46 | }
47 |
48 | @available(iOS 14.0, *)
49 | extension View {
50 | public func indicatorStyle(_ style: some PagingIndicatorStyle) -> some View {
51 | environment(\.indicatorStyle, style)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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 |
33 | /// The PagingIndexable protocol is used to compare items in your
34 | /// menu. Conform to this protocol when you need to mix multiple
35 | /// PagingItem types that all need to be compared.
36 | //
37 | /// The PagingIndexable protocol requires the conforming type to
38 | /// provide an index property of type Int, which is used to compare
39 | /// items in the menu.
40 | ///
41 | /// For example, if you have a menu that contains both PagingIndexItem
42 | /// and PagingImageItem types, you can conform both types to
43 | /// PagingIndexable and Parchment will provide a default
44 | /// implementation that will be used to animate between them.
45 | public protocol PagingIndexable {
46 | var index: Int { get }
47 | }
48 |
49 | extension PagingItem where Self: PagingIndexable {
50 | public func isBefore(item: PagingItem) -> Bool {
51 | guard let item = item as? PagingIndexable else { return false }
52 | return index < item.index
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/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 | @MainActor
7 | init()
8 | }
9 |
10 | @MainActor
11 | func createLayout(layout: T.Type) -> T where T: PagingLayout {
12 | return layout.init()
13 | }
14 |
--------------------------------------------------------------------------------
/Parchment/Protocols/PagingMenuDataSource.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol PagingMenuDataSource: AnyObject {
4 | @MainActor
5 | func pagingItemBefore(pagingItem: PagingItem) -> PagingItem?
6 | @MainActor
7 | func pagingItemAfter(pagingItem: PagingItem) -> PagingItem?
8 | }
9 |
--------------------------------------------------------------------------------
/Parchment/Protocols/PagingMenuDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol PagingMenuDelegate: AnyObject {
4 | @MainActor
5 | func selectContent(pagingItem: PagingItem, direction: PagingDirection, animated: Bool)
6 | @MainActor
7 | func removeContent()
8 | }
9 |
--------------------------------------------------------------------------------
/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 | @MainActor
18 | func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int
19 |
20 | /// Return the view controller associated with a given index. This
21 | /// method is only called for the currently selected `PagingItem`,
22 | /// and its two possible siblings.
23 | ///
24 | /// - Parameter pagingViewController: The `PagingViewController`
25 | /// instance
26 | /// - Parameter index: The index of a given `PagingItem`
27 | /// - Returns: The view controller for the given index
28 | @MainActor
29 | func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController
30 |
31 | /// Return the `PagingItem` instance for a given index
32 | ///
33 | /// - Parameter pagingViewController: The `PagingViewController`
34 | /// instance
35 | /// - Returns: The `PagingItem` instance
36 | @MainActor
37 | func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem
38 | }
39 |
--------------------------------------------------------------------------------
/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 associated 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 | @MainActor
20 | func pagingViewController(_: PagingViewController, viewControllerFor pagingItem: PagingItem) -> UIViewController
21 |
22 | /// The `PagingItem` that comes before a given `PagingItem`
23 | ///
24 | /// - Parameter pagingViewController: The `PagingViewController`
25 | /// instance
26 | /// - Parameter pagingItemBeforePagingItem: A `PagingItem` instance
27 | /// - Returns: The `PagingItem` that appears before the given
28 | /// `PagingItem`, or `nil` to indicate that no more progress can be
29 | /// made in that direction.
30 | @MainActor
31 | func pagingViewController(_: PagingViewController, itemBefore pagingItem: PagingItem) -> PagingItem?
32 |
33 | /// The `PagingItem` that comes after a given `PagingItem`
34 | ///
35 | /// - Parameter pagingViewController: The `PagingViewController`
36 | /// instance
37 | /// - Parameter pagingItemAfterPagingItem: A `PagingItem` instance
38 | /// - Returns: The `PagingItem` that appears after the given
39 | /// `PagingItem`, or `nil` to indicate that no more progress can be
40 | /// made in that direction.
41 | @MainActor
42 | func pagingViewController(_: PagingViewController, itemAfter pagingItem: PagingItem) -> PagingItem?
43 | }
44 |
--------------------------------------------------------------------------------
/Parchment/Protocols/PagingViewControllerSizeDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @MainActor
4 | public protocol PagingViewControllerSizeDelegate: AnyObject {
5 | /// Manually control the width for a given `PagingItem`. Parchment
6 | /// does not support self-sizing cells, so you have to use this if
7 | /// you have a cell that you want to size based on its content.
8 | ///
9 | /// - Parameter pagingViewController: The `PagingViewController`
10 | /// instance
11 | /// - Parameter pagingItem: The `PagingItem` instance
12 | /// - Parameter isSelected: A boolean that indicates whether the
13 | /// given `PagingItem` is selected
14 | /// - Returns: The width for the `PagingItem`
15 | func pagingViewController(
16 | _: PagingViewController,
17 | widthForPagingItem pagingItem: PagingItem,
18 | isSelected: Bool
19 | ) -> CGFloat
20 | }
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Structs/PageItem.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(iOS 14.0, *)
4 | struct PageItem: PagingItem, Hashable, Comparable {
5 | let identifier: Int
6 | let index: Int
7 | let page: Page
8 |
9 | func hash(into hasher: inout Hasher) {
10 | hasher.combine(identifier)
11 | hasher.combine(index)
12 | }
13 |
14 | static func == (lhs: PageItem, rhs: PageItem) -> Bool {
15 | return lhs.identifier == rhs.identifier && lhs.index == rhs.index
16 | }
17 |
18 | static func < (lhs: PageItem, rhs: PageItem) -> Bool {
19 | return lhs.index < rhs.index
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Parchment/Structs/PageItemBuilder.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @available(iOS 14.0, *)
4 | @resultBuilder
5 | public struct PageBuilder {
6 | public static func buildExpression(_ expression: Page) -> [Page] {
7 | return [expression]
8 | }
9 |
10 | public static func buildExpression(_ expression: Page?) -> [Page] {
11 | if let expression = expression {
12 | return [expression]
13 | }
14 | return []
15 | }
16 |
17 | public static func buildExpression(_ expression: [Page]) -> [Page] {
18 | return expression
19 | }
20 |
21 | public static func buildBlock(_ components: [Page]) -> [Page] {
22 | return components
23 | }
24 |
25 | public static func buildBlock(_ components: [Page]...) -> [Page] {
26 | return components.reduce([], +)
27 | }
28 |
29 | public static func buildArray(_ components: [[Page]]) -> [Page] {
30 | return components.flatMap { $0 }
31 | }
32 |
33 | public static func buildOptional(_ component: [Page]?) -> [Page] {
34 | return component ?? []
35 | }
36 |
37 | public static func buildEither(first component: [Page]) -> [Page] {
38 | return component
39 | }
40 |
41 | public static func buildEither(second component: [Page]) -> [Page] {
42 | return component
43 | }
44 |
45 | public static func buildFor(_ component: [Page]) -> [Page] {
46 | return component
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Parchment/Structs/PageItemCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Foundation
3 |
4 | @available(iOS 14.0, *)
5 | final class PageItemCell: PagingCell {
6 | private var page: Page!
7 | private var options: PagingOptions?
8 | private var itemSelected: Bool = false
9 |
10 | override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options: PagingOptions) {
11 | let item = pagingItem as! PageItem
12 | let state = PageState(progress: selected ? 1 : 0, isSelected: selected)
13 |
14 | self.page = item.page
15 | self.options = options
16 | self.itemSelected = selected
17 |
18 | contentConfiguration = page.header(options, state)
19 | backgroundColor = selected ? options.selectedBackgroundColor : options.backgroundColor
20 | }
21 |
22 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
23 | super.apply(layoutAttributes)
24 | if let attributes = layoutAttributes as? PagingCellLayoutAttributes, let options = options {
25 | let state = PageState(progress: attributes.progress, isSelected: itemSelected)
26 | contentConfiguration = page.header(options, state)
27 | backgroundColor = UIColor.interpolate(
28 | from: options.backgroundColor,
29 | to: options.selectedBackgroundColor,
30 | with: attributes.progress
31 | )
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Parchment/Structs/PageState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Represents the current state of a page. This will be passed into
4 | /// the `Page` struct while scrolling, and can be used to update the
5 | /// appearance of the corresponding menu item to reflect the current
6 | /// progress and selection state.
7 | public struct PageState {
8 | public let progress: CGFloat
9 | public let isSelected: Bool
10 | }
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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, PagingIndexable, Hashable {
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Parchment/Structs/PagingNavigationOrientation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum PagingNavigationOrientation {
4 | case vertical
5 | case horizontal
6 | }
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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: Equatable {
10 | @MainActor
11 | static var callCount: Int = 0
12 |
13 | let index: Int
14 | let action: Action
15 |
16 | @MainActor
17 | init(action: Action) {
18 | Self.callCount += 1
19 | self.index = Self.callCount
20 | self.action = action
21 | }
22 | }
23 |
24 | extension MockCall: Comparable {
25 | static func < (lhs: MockCall, rhs: MockCall) -> Bool {
26 | return lhs.index < rhs.index
27 | }
28 | }
29 |
30 | @MainActor
31 | protocol Mock {
32 | var calls: [MockCall] { get }
33 | }
34 |
35 | func actions(_ calls: [MockCall]) -> [Action] {
36 | return calls.map { $0.action }
37 | }
38 |
39 | func combinedActions(_ a: [MockCall], _ b: [MockCall]) -> [Action] {
40 | return actions(Array(a + b).sorted())
41 | }
42 |
43 | func combinedActions(_ a: [MockCall], _ b: [MockCall], _ c: [MockCall]) -> [Action] {
44 | return actions(Array(a + b + c).sorted())
45 | }
46 |
--------------------------------------------------------------------------------
/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 | action: .collectionViewLayout(.prepare)
21 | ))
22 | }
23 |
24 | func invalidateLayout() {
25 | calls.append(MockCall(
26 | action: .collectionViewLayout(.invalidateLayout)
27 | ))
28 | }
29 |
30 | func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
31 | let context = context as! PagingInvalidationContext
32 | calls.append(MockCall(
33 | action: .collectionViewLayout(.invalidateLayoutWithContext(
34 | invalidateSizes: context.invalidateSizes
35 | ))
36 | ))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Mocks/MockPagingControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import Parchment
3 |
4 | @MainActor
5 | final class MockPagingControllerDelegate: PagingMenuDelegate, Mock {
6 | enum Action: Equatable {
7 | case selectContent(pagingItem: Item, direction: PagingDirection, animated: Bool)
8 | case removeContent
9 | }
10 |
11 | var calls: [MockCall] = []
12 |
13 | func selectContent(pagingItem: PagingItem, direction: PagingDirection, animated: Bool) {
14 | calls.append(MockCall(
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 | action: .delegate(.removeContent)
26 | ))
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ParchmentTests/PagingDataStructureTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Testing
3 | @testable import Parchment
4 |
5 | struct PagingDataTests {
6 | private let visibleItems: PagingItems
7 |
8 | init() {
9 | visibleItems = PagingItems(items: [
10 | Item(index: 0),
11 | Item(index: 1),
12 | Item(index: 2),
13 | ])
14 | }
15 |
16 | @Test func indexPathForPagingItemFound() {
17 | let indexPath = visibleItems.indexPath(for: Item(index: 0))!
18 | #expect(indexPath.item == 0)
19 | }
20 |
21 | @Test func indexPathForPagingItemMissing() {
22 | let indexPath = visibleItems.indexPath(for: Item(index: -1))
23 | #expect(indexPath == nil)
24 | }
25 |
26 | @Test func pagingItemForIndexPath() {
27 | let indexPath = IndexPath(item: 0, section: 0)
28 | let pagingItem = visibleItems.pagingItem(for: indexPath) as! Item
29 | #expect(pagingItem == Item(index: 0))
30 | }
31 |
32 | @Test func directionForIndexPathForward() {
33 | let currentPagingItem = Item(index: 0)
34 | let upcomingPagingItem = Item(index: 1)
35 | let direction = visibleItems.direction(from: currentPagingItem, to: upcomingPagingItem)
36 | #expect(direction == PagingDirection.forward(sibling: true))
37 | }
38 |
39 | @Test func directionForIndexPathReverse() {
40 | let currentPagingItem = Item(index: 1)
41 | let upcomingPagingItem = Item(index: 0)
42 | let direction = visibleItems.direction(from: currentPagingItem, to: upcomingPagingItem)
43 | #expect(direction == PagingDirection.reverse(sibling: true))
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ParchmentTests/PagingIndicatorLayoutAttributesTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Testing
3 | @testable import Parchment
4 |
5 | @MainActor
6 | struct PagingIndicatorLayoutAttributesTests {
7 | private let layoutAttributes = PagingIndicatorLayoutAttributes()
8 | private let options: PagingOptions
9 |
10 | init() {
11 | var options = PagingOptions()
12 | options.font = UIFont.systemFont(ofSize: 15)
13 | options.selectedFont = UIFont.boldSystemFont(ofSize: 15)
14 | options.textColor = .blue
15 | options.selectedTextColor = .red
16 | options.indicatorColor = .green
17 | options.indicatorOptions = .visible(
18 | height: 20,
19 | zIndex: Int.max,
20 | spacing: UIEdgeInsets(),
21 | insets: UIEdgeInsets()
22 | )
23 | self.options = options
24 | }
25 |
26 | @Test func configure() {
27 | layoutAttributes.configure(options)
28 |
29 | #expect(layoutAttributes.backgroundColor == UIColor.green)
30 | #expect(layoutAttributes.frame.height == 20)
31 | #expect(layoutAttributes.frame.origin.y == 20)
32 | #expect(layoutAttributes.zIndex == Int.max)
33 | }
34 |
35 | @Test func tweening() {
36 | layoutAttributes.configure(options)
37 |
38 | let from = PagingIndicatorMetric(
39 | frame: CGRect(x: 0, y: 0, width: 200, height: 0),
40 | insets: .left(50),
41 | spacing: UIEdgeInsets()
42 | )
43 |
44 | let to = PagingIndicatorMetric(
45 | frame: CGRect(x: 200, y: 0, width: 100, height: 0),
46 | insets: .right(50),
47 | spacing: UIEdgeInsets()
48 | )
49 |
50 | layoutAttributes.update(from: from, to: to, progress: 0)
51 | #expect(layoutAttributes.frame == CGRect(x: 50, y: 20, width: 150, height: 20))
52 |
53 | layoutAttributes.update(from: from, to: to, progress: 1)
54 | #expect(layoutAttributes.frame == CGRect(x: 200, y: 20, width: 50, height: 20))
55 |
56 | layoutAttributes.update(from: from, to: to, progress: 0.5)
57 | #expect(layoutAttributes.frame == CGRect(x: 125, y: 20, width: 100, height: 20))
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ParchmentTests/PagingStateTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Testing
3 | @testable import Parchment
4 |
5 | struct PagingStateTests {
6 | @Test func selected() {
7 | let state: PagingState = .selected(pagingItem: Item(index: 0))
8 |
9 | #expect(state.currentPagingItem as? Item? == Item(index: 0))
10 | #expect(state.upcomingPagingItem == nil)
11 | #expect(state.progress == 0)
12 | #expect(state.visuallySelectedPagingItem as? Item? == Item(index: 0))
13 | }
14 |
15 | @Test func scrollingCurrentPagingItem() {
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 | #expect(state.currentPagingItem as? Item? == Item(index: 0))
25 | }
26 |
27 | @Test func progress() {
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 | #expect(state.progress == 0.5)
37 | }
38 |
39 | @Test func upcomingPagingItem() {
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 | #expect(state.upcomingPagingItem as? Item? == Item(index: 1))
49 | }
50 |
51 | @Test func upcomingPagingItemNil() {
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 | #expect(state.upcomingPagingItem == nil)
61 | }
62 |
63 | @Test func visuallySelectedPagingItemProgressLarge() {
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 | #expect(state.visuallySelectedPagingItem as? Item? == Item(index: 1))
73 | }
74 |
75 | @Test func visuallySelectedPagingItemProgressSmall() {
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 | #expect(state.visuallySelectedPagingItem as? Item? == Item(index: 0))
85 | }
86 |
87 | @Test func visuallySelectedPagingItemUpcomingPagingItemNil() {
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 | #expect(state.visuallySelectedPagingItem as? Item? == Item(index: 0))
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/ParchmentTests/PagingViewTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import Parchment
3 |
4 | @MainActor
5 | final class PagingViewTests {
6 | private let pagingView: PagingView
7 | private let collectionView: UICollectionView
8 |
9 | init() {
10 | let options = PagingOptions()
11 | let pageView = UIView(frame: .zero)
12 |
13 | collectionView = UICollectionView(
14 | frame: .zero,
15 | collectionViewLayout: UICollectionViewLayout()
16 | )
17 |
18 | pagingView = PagingView(
19 | options: options,
20 | collectionView: collectionView,
21 | pageView: pageView
22 | )
23 | }
24 |
25 | @Test func menuBackgroundColor() {
26 | pagingView.configure()
27 |
28 | var options = PagingOptions()
29 | options.menuBackgroundColor = .green
30 | pagingView.options = options
31 |
32 | #expect(collectionView.backgroundColor == .green)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ParchmentTests/Resources.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
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 |
--------------------------------------------------------------------------------
/ParchmentTests/Resources.xcassets/Green.imageset/Green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechsteiner/Parchment/dfb23ea5118ca8bfbc578065627fccf4ec4a362e/ParchmentTests/Resources.xcassets/Green.imageset/Green.png
--------------------------------------------------------------------------------
/ParchmentTests/UIColorInterpolationTests.swift:
--------------------------------------------------------------------------------
1 | import CoreGraphics
2 | import Testing
3 | @testable import Parchment
4 |
5 | final class UIColorInterpolationTests {
6 | // Colors initialized with UIColor(patternImage:) have only 1
7 | // color component. This test ensures we don't crash.
8 | @Test func imageFromPatternImageDefaultToBlack() {
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 | #expect(result == UIColor(red: 0, green: 0, blue: 0, alpha: 1))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ParchmentTests/Utilities/CreateDistance.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import Parchment
3 |
4 | @MainActor
5 | func createDistance(
6 | bounds: CGRect = .zero,
7 | contentSize: CGSize? = nil,
8 | currentItem: Item,
9 | currentItemBounds: CGRect?,
10 | upcomingItem: Item,
11 | upcomingItemBounds: CGRect,
12 | sizeCache: PagingSizeCache,
13 | selectedScrollPosition: PagingSelectedScrollPosition,
14 | navigationOrientation: PagingNavigationOrientation
15 | ) -> PagingDistance {
16 | let collectionView = MockCollectionView()
17 | collectionView.contentOffset = bounds.origin
18 | collectionView.bounds = bounds
19 |
20 | if let contentSize = contentSize {
21 | collectionView.contentSize = contentSize
22 | } else if let currentItemBounds = currentItemBounds {
23 | let contentFrame = currentItemBounds.union(upcomingItemBounds)
24 | collectionView.contentSize = contentFrame.size
25 | } else {
26 | collectionView.contentSize = upcomingItemBounds.size
27 | }
28 |
29 | let visibleItems = PagingItems(items: [currentItem, upcomingItem])
30 | var layoutAttributes: [IndexPath: PagingCellLayoutAttributes] = [:]
31 |
32 | if let currentItemBounds = currentItemBounds {
33 | let currentIndexPath = visibleItems.indexPath(for: currentItem)!
34 | let currentAttributes = PagingCellLayoutAttributes(forCellWith: currentIndexPath)
35 | currentAttributes.frame = currentItemBounds
36 | layoutAttributes[currentIndexPath] = currentAttributes
37 | }
38 |
39 | let upcomingIndexPath = visibleItems.indexPath(for: upcomingItem)!
40 | let upcomingAttributes = PagingCellLayoutAttributes(forCellWith: upcomingIndexPath)
41 | upcomingAttributes.frame = upcomingItemBounds
42 | layoutAttributes[upcomingIndexPath] = upcomingAttributes
43 |
44 | return PagingDistance(
45 | view: collectionView,
46 | currentPagingItem: currentItem,
47 | upcomingPagingItem: upcomingItem,
48 | visibleItems: visibleItems,
49 | sizeCache: sizeCache,
50 | selectedScrollPosition: selectedScrollPosition,
51 | layoutAttributes: layoutAttributes,
52 | navigationOrientation: navigationOrientation
53 | )!
54 | }
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ParchmentUITests/ParchmentUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @MainActor
4 | final class ParchmentUITests: XCTestCase {
5 | override func setUp() {
6 | continueAfterFailure = false
7 | }
8 |
9 | func testSelect() {
10 | let app = XCUIApplication()
11 | app.launchArguments = ["--ui-testing"]
12 | app.launch()
13 |
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 | let app = XCUIApplication()
30 | app.launchArguments = ["--ui-testing"]
31 | app.launch()
32 |
33 | app.scrollViews.firstMatch.swipeLeft()
34 | let content1 = app.scrollViews.firstMatch.staticTexts["1"]
35 | XCTAssertTrue(content1.waitForExistence(timeout: 1))
36 |
37 | let cell1 = app.collectionViews.cells["View 1"]
38 | XCTAssertTrue(cell1.isSelected)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------