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