diff --git a/.gitignore b/.gitignore
index 1e13f563..b4306f74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Xcode
#
build/
+.build/
*.pbxuser
!default.pbxuser
*.mode1v3
diff --git a/Example/Examples/Header/HeaderViewController.swift b/Example/Examples/Header/HeaderViewController.swift
index 07fc23c0..75f8599e 100644
--- a/Example/Examples/Header/HeaderViewController.swift
+++ b/Example/Examples/Header/HeaderViewController.swift
@@ -10,9 +10,9 @@ import UIKit
class HeaderPagingView: PagingView {
static let HeaderHeight: CGFloat = 200
- var headerHeightConstraint: NSLayoutConstraint?
+ var headerHeightConstraint: NSLayoutConstraint!
- private lazy var headerView: UIImageView = {
+ private(set) lazy var headerView: UIImageView = {
let view = UIImageView(image: UIImage(named: "Header"))
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
@@ -29,7 +29,12 @@ class HeaderPagingView: PagingView {
headerHeightConstraint = headerView.heightAnchor.constraint(
equalToConstant: HeaderPagingView.HeaderHeight
)
- headerHeightConstraint?.isActive = true
+ headerHeightConstraint.isActive = true
+ headerHeightConstraint.priority = .defaultLow
+
+ let bottomConstraint = headerView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor)
+ bottomConstraint.isActive = true
+ bottomConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
@@ -73,9 +78,8 @@ class HeaderViewController: UIViewController {
private let pagingViewController = HeaderPagingViewController()
- private var headerConstraint: NSLayoutConstraint {
- let pagingView = pagingViewController.view as! HeaderPagingView
- return pagingView.headerHeightConstraint!
+ private var pagingView: HeaderPagingView {
+ return pagingViewController.view as! HeaderPagingView
}
override func viewDidLoad() {
@@ -128,7 +132,8 @@ extension HeaderViewController: PagingViewControllerDataSource {
let height = pagingViewController.options.menuHeight + HeaderPagingView.HeaderHeight
let insets = UIEdgeInsets(top: height, left: 0, bottom: 0, right: 0)
viewController.tableView.contentInset = insets
- viewController.tableView.scrollIndicatorInsets = insets
+ viewController.tableView.scrollIndicatorInsets = UIEdgeInsets(top: height, left: 0, bottom: 0, right: 0)
+ viewController.tableView.contentOffset.y = -insets.top
return viewController
}
@@ -155,46 +160,34 @@ extension HeaderViewController: PagingViewControllerDelegate {
}
}
- func pagingViewController(_: PagingViewController, willScrollToItem _: PagingItem, startingViewController _: UIViewController, destinationViewController: UIViewController) {
+ func pagingViewController(_: PagingViewController, isScrollingFromItem currentPagingItem: PagingItem, toItem upcomingPagingItem: PagingItem?, startingViewController: UIViewController, destinationViewController: UIViewController?, progress: CGFloat) {
guard let destinationViewController = destinationViewController as? TableViewController else { return }
// Update the content offset based on the height of the header
// view. This ensures that the content offset is correct if you
// swipe to a new page while the header view is hidden.
if let scrollView = destinationViewController.tableView {
- let offset = headerConstraint.constant + pagingViewController.options.menuHeight
+ let offset = pagingView.headerView.bounds.height + pagingViewController.options.menuHeight
scrollView.contentOffset = CGPoint(x: 0, y: -offset)
- updateScrollIndicatorInsets(in: scrollView)
}
}
}
extension HeaderViewController: UITableViewDelegate {
- func updateScrollIndicatorInsets(in scrollView: UIScrollView) {
- let offset = min(0, scrollView.contentOffset.y) * -1
- let insetTop = max(pagingViewController.options.menuHeight, offset)
- let insets = UIEdgeInsets(top: insetTop, left: 0, bottom: 0, right: 0)
- scrollView.scrollIndicatorInsets = insets
- }
-
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.y < 0 else {
// Reset the header constraint in case we scrolled so fast that
// the height was not set to zero before the content offset
// became negative.
- if headerConstraint.constant > 0 {
- headerConstraint.constant = 0
+ if pagingView.headerHeightConstraint.constant > 0 {
+ pagingView.headerHeightConstraint.constant = 0
}
return
}
- // Update the scroll indicator insets so they move alongside the
- // header view when scrolling.
- updateScrollIndicatorInsets(in: scrollView)
-
// Update the height of the header view based on the content
// offset of the currently selected view controller.
let height = max(0, abs(scrollView.contentOffset.y) - pagingViewController.options.menuHeight)
- headerConstraint.constant = height
+ pagingView.headerHeightConstraint.constant = height
}
}
diff --git a/Example/Examples/Icons/IconsViewController.swift b/Example/Examples/Icons/IconsViewController.swift
index cc4b7bf0..80cdeeee 100644
--- a/Example/Examples/Icons/IconsViewController.swift
+++ b/Example/Examples/Icons/IconsViewController.swift
@@ -1,30 +1,17 @@
import Parchment
import UIKit
-struct IconItem: PagingItem, Hashable {
+struct IconItem: PagingItem, PagingIndexable, Hashable {
+ let identifier: Int
let icon: String
let index: Int
let image: UIImage?
init(icon: String, index: Int) {
+ self.identifier = icon.hashValue
self.icon = icon
self.index = index
- image = UIImage(named: icon)
- }
-
- /// By default, isBefore is implemented when the PagingItem conforms
- /// to Comparable, but in this case we want a custom implementation
- /// where we also compare IconItem with PagingIndexItem. This
- /// ensures that we animate the page transition in the correct
- /// direction when selecting items.
- func isBefore(item: PagingItem) -> Bool {
- if let item = item as? PagingIndexItem {
- return index < item.index
- } else if let item = item as? Self {
- return index < item.index
- } else {
- return false
- }
+ self.image = UIImage(named: icon)
}
}
@@ -64,7 +51,7 @@ class IconsViewController: UIViewController {
pagingViewController.select(pagingItem: IconItem(icon: icons[0], index: 0))
// Add the paging view controller as a child view controller
- // and contrain it to all edges.
+ // and constrain it to all edges.
addChild(pagingViewController)
view.addSubview(pagingViewController.view)
view.constrainToEdges(pagingViewController.view)
diff --git a/Example/Examples/Images/UnsplashViewController.swift b/Example/Examples/Images/UnsplashViewController.swift
index c89b5668..e7bf2cea 100644
--- a/Example/Examples/Images/UnsplashViewController.swift
+++ b/Example/Examples/Images/UnsplashViewController.swift
@@ -34,12 +34,12 @@ class ImagePagingView: PagingView {
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
- collectionView.topAnchor.constraint(equalTo: topAnchor),
+ collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
pageView.leadingAnchor.constraint(equalTo: leadingAnchor),
pageView.trailingAnchor.constraint(equalTo: trailingAnchor),
pageView.bottomAnchor.constraint(equalTo: bottomAnchor),
- pageView.topAnchor.constraint(equalTo: topAnchor),
+ pageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
])
}
}
diff --git a/Example/Examples/LargeTitles/LargeTitlesViewController.swift b/Example/Examples/LargeTitles/LargeTitlesViewController.swift
index 56ae1190..a27e32ba 100644
--- a/Example/Examples/LargeTitles/LargeTitlesViewController.swift
+++ b/Example/Examples/LargeTitles/LargeTitlesViewController.swift
@@ -58,27 +58,8 @@ class LargeTitlesViewController: UIViewController {
// Tell the navigation bar that we want to have large titles
navigationController.navigationBar.prefersLargeTitles = true
- // Customize the menu to match the navigation bar color
- let blue = UIColor(red: 3 / 255, green: 125 / 255, blue: 233 / 255, alpha: 1)
-
- if #available(iOS 13.0, *) {
- let appearance = UINavigationBarAppearance()
- appearance.backgroundColor = blue
- appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
- appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
-
- UINavigationBar.appearance().tintColor = .white
- UINavigationBar.appearance().standardAppearance = appearance
- UINavigationBar.appearance().compactAppearance = appearance
- UINavigationBar.appearance().scrollEdgeAppearance = appearance
- } else {
- UINavigationBar.appearance().tintColor = .white
- UINavigationBar.appearance().barTintColor = blue
- UINavigationBar.appearance().isTranslucent = false
- }
-
view.backgroundColor = .white
- pagingViewController.menuBackgroundColor = blue
+ pagingViewController.menuBackgroundColor = .systemBlue
pagingViewController.menuItemSize = .fixed(width: 150, height: 30)
pagingViewController.textColor = UIColor.white.withAlphaComponent(0.7)
pagingViewController.selectedTextColor = UIColor.white
@@ -148,6 +129,7 @@ extension LargeTitlesViewController: PagingViewControllerDataSource {
let insets = UIEdgeInsets(top: pagingViewController.options.menuItemSize.height, left: 0, bottom: 0, right: 0)
viewController.tableView.scrollIndicatorInsets = insets
viewController.tableView.contentInset = insets
+ viewController.tableView.contentOffset.y = -insets.top
return viewController
}
diff --git a/Example/Examples/NavigationBar/NavigationBarViewController.swift b/Example/Examples/NavigationBar/NavigationBarViewController.swift
index 8d1b7e35..6e03b7a3 100644
--- a/Example/Examples/NavigationBar/NavigationBarViewController.swift
+++ b/Example/Examples/NavigationBar/NavigationBarViewController.swift
@@ -45,14 +45,14 @@ class NavigationBarViewController: UIViewController {
pagingViewController.selectedTextColor = .white
// Make sure you add the PagingViewController as a child view
- // controller and contrain it to the edges of the view.
+ // controller and constrain it to the edges of the view.
addChild(pagingViewController)
view.addSubview(pagingViewController.view)
view.constrainToEdges(pagingViewController.view)
pagingViewController.didMove(toParent: self)
// Set the menu view as the title view on the navigation bar. This
- // will remove the menu view from the view hierachy and put it
+ // will remove the menu view from the view hierarchy and put it
// into the navigation bar.
navigationItem.titleView = pagingViewController.collectionView
}
diff --git a/Example/Examples/Scroll/ScrollViewController.swift b/Example/Examples/Scroll/ScrollViewController.swift
index 3591c205..4dd07571 100644
--- a/Example/Examples/Scroll/ScrollViewController.swift
+++ b/Example/Examples/Scroll/ScrollViewController.swift
@@ -49,19 +49,21 @@ class ScrollViewController: UIViewController {
super.viewDidLoad()
// Add the paging view controller as a child view controller and
- // contrain it to all edges.
+ // constrain it to all edges.
addChild(pagingViewController)
view.addSubview(pagingViewController.view)
view.constrainToEdges(pagingViewController.view)
pagingViewController.didMove(toParent: self)
+ // Prevent the menu from showing when scrolled out of view.
+ pagingViewController.view.clipsToBounds = true
+
// Set our data source and delegate.
pagingViewController.dataSource = self
pagingViewController.delegate = self
}
- /// Calculate the menu offset based on the content offset of the
- /// scroll view.
+ /// Calculate the menu offset based on the content offset of the scroll view.
private func menuOffset(for scrollView: UIScrollView) -> CGFloat {
return min(pagingViewController.options.menuHeight, max(0, scrollView.contentOffset.y))
}
@@ -80,6 +82,9 @@ extension ScrollViewController: PagingViewControllerDataSource {
// Set delegate so that we can listen to scroll events.
viewController.tableView.delegate = self
+ // Ensure that the scroll view is scroll to the top.
+ viewController.tableView.contentOffset.y = -insets.top
+
return viewController
}
@@ -94,8 +99,13 @@ extension ScrollViewController: PagingViewControllerDataSource {
extension ScrollViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
- // Offset the menu view based on the content offset of the
- // scroll view.
+ // Only update the menu when the currently selected view is scrolled.
+ guard
+ let selectedViewController = pagingViewController.pageViewController.selectedViewController as? TableViewController,
+ selectedViewController.tableView === scrollView
+ else { return }
+
+ // Offset the menu view based on the content offset of the scroll view.
if let menuView = pagingViewController.view as? ScrollPagingView {
menuView.menuTopConstraint?.constant = -menuOffset(for: scrollView)
}
@@ -116,6 +126,11 @@ extension ScrollViewController: PagingViewControllerDelegate {
let to = menuOffset(for: destinationViewController.tableView)
let offset = ((to - from) * abs(progress)) + from
+ // Reset the content offset when scrolling to a new page. You
+ // could also remove this, and it will hide the menu when
+ // swiping back to the previous page.
+ destinationViewController.tableView.contentOffset.y = -pagingViewController.options.menuHeight
+
menuView.menuTopConstraint?.constant = -offset
}
}
diff --git a/Example/Examples/Wheel/WheelViewController.swift b/Example/Examples/Wheel/WheelViewController.swift
new file mode 100644
index 00000000..84b1856a
--- /dev/null
+++ b/Example/Examples/Wheel/WheelViewController.swift
@@ -0,0 +1,28 @@
+import Parchment
+import UIKit
+
+final class WheelViewController: UIViewController {
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let viewControllers = [
+ ContentViewController(index: 0),
+ ContentViewController(index: 1),
+ ContentViewController(index: 2),
+ ContentViewController(index: 3),
+ ContentViewController(index: 4),
+ ContentViewController(index: 5),
+ ContentViewController(index: 6),
+ ]
+
+ let pagingViewController = PagingViewController(viewControllers: viewControllers)
+ pagingViewController.menuInteraction = .wheel
+ pagingViewController.selectedScrollPosition = .center
+ pagingViewController.menuItemSize = .fixed(width: 100, height: 60)
+
+ addChild(pagingViewController)
+ view.addSubview(pagingViewController.view)
+ view.constrainToEdges(pagingViewController.view)
+ pagingViewController.didMove(toParent: self)
+ }
+}
diff --git a/Example/ExamplesViewController.swift b/Example/ExamplesViewController.swift
index 3c668bca..4d957494 100644
--- a/Example/ExamplesViewController.swift
+++ b/Example/ExamplesViewController.swift
@@ -5,6 +5,7 @@ enum Example: CaseIterable {
case selfSizing
case calendar
case sizeDelegate
+ case wheel
case images
case icons
case storyboard
@@ -25,6 +26,8 @@ enum Example: CaseIterable {
return "Calendar"
case .sizeDelegate:
return "Size delegate"
+ case .wheel:
+ return "Wheel"
case .images:
return "Images"
case .icons:
@@ -82,7 +85,7 @@ final class ExamplesViewController: UITableViewController {
switch example {
case .largeTitles:
- let navigationController = UINavigationController(rootViewController: viewController)
+ let navigationController = NavigationController(rootViewController: viewController)
navigationController.modalPresentationStyle = .fullScreen
viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done,
@@ -106,6 +109,8 @@ final class ExamplesViewController: UITableViewController {
return SelfSizingViewController()
case .sizeDelegate:
return SizeDelegateViewController(nibName: nil, bundle: nil)
+ case .wheel:
+ return WheelViewController()
case .images:
return UnsplashViewController(nibName: nil, bundle: nil)
case .icons:
@@ -131,3 +136,32 @@ final class ExamplesViewController: UITableViewController {
dismiss(animated: true)
}
}
+
+final class NavigationController: UINavigationController {
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .lightContent
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ let appearance = UINavigationBarAppearance()
+ appearance.backgroundColor = .systemBlue
+ appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
+ appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
+ navigationBar.tintColor = .white
+ navigationBar.standardAppearance = appearance
+ navigationBar.compactAppearance = appearance
+ navigationBar.scrollEdgeAppearance = appearance
+ }
+
+ // For debugging purposes. Adds a hook to push a new view
+ // controller to debug navigation controller related issues.
+ override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
+ if motion == .motionShake {
+ let viewController = UIViewController()
+ viewController.title = "Page"
+ viewController.view.backgroundColor = .white
+ pushViewController(viewController, animated: true)
+ }
+ }
+}
diff --git a/Example/Resources/Base.lproj/Main.storyboard b/Example/Resources/Base.lproj/Main.storyboard
index d0dfcf93..5174893f 100644
--- a/Example/Resources/Base.lproj/Main.storyboard
+++ b/Example/Resources/Base.lproj/Main.storyboard
@@ -1,24 +1,29 @@
-
+
-
+
-
+
-
-
-
-
+
+
+
-
+
+
+
+
+
+
+
@@ -27,6 +32,9 @@
+
+
+
@@ -36,19 +44,19 @@
-
+
-
+
-
+
-
+
diff --git a/Example/Resources/Info.plist b/Example/Resources/Info.plist
index 40c6215d..1409b3fa 100644
--- a/Example/Resources/Info.plist
+++ b/Example/Resources/Info.plist
@@ -43,5 +43,9 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ UIViewControllerBasedStatusBarAppearance
+
+ UIStatusBarStyle
+ UIStatusBarStyleLightContent
diff --git a/Example/Resources/UIView+constraints.swift b/Example/Resources/UIView+constraints.swift
index ce64b38b..74f5349f 100644
--- a/Example/Resources/UIView+constraints.swift
+++ b/Example/Resources/UIView+constraints.swift
@@ -55,51 +55,11 @@ extension UIView {
func constrainToEdges(_ subview: UIView) {
subview.translatesAutoresizingMaskIntoConstraints = false
- let topContraint = NSLayoutConstraint(
- item: subview,
- attribute: .top,
- relatedBy: .equal,
- toItem: self,
- attribute: .top,
- multiplier: 1.0,
- constant: 0
- )
-
- let bottomConstraint = NSLayoutConstraint(
- item: subview,
- attribute: .bottom,
- relatedBy: .equal,
- toItem: self,
- attribute: .bottom,
- multiplier: 1.0,
- constant: 0
- )
-
- let leadingContraint = NSLayoutConstraint(
- item: subview,
- attribute: .leading,
- relatedBy: .equal,
- toItem: self,
- attribute: .leading,
- multiplier: 1.0,
- constant: 0
- )
-
- let trailingContraint = NSLayoutConstraint(
- item: subview,
- attribute: .trailing,
- relatedBy: .equal,
- toItem: self,
- attribute: .trailing,
- multiplier: 1.0,
- constant: 0
- )
-
- addConstraints([
- topContraint,
- bottomConstraint,
- leadingContraint,
- trailingContraint,
+ NSLayoutConstraint.activate([
+ subview.leadingAnchor.constraint(equalTo: leadingAnchor),
+ subview.trailingAnchor.constraint(equalTo: trailingAnchor),
+ subview.bottomAnchor.constraint(equalTo: bottomAnchor),
+ subview.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor)
])
}
}
diff --git a/ExampleSwiftUI/ChangeItems.swift b/ExampleSwiftUI/ChangeItems.swift
deleted file mode 100644
index d01da057..00000000
--- a/ExampleSwiftUI/ChangeItems.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-import Parchment
-import SwiftUI
-import UIKit
-
-struct ChangeItemsView: View {
- @State var items = [
- PagingIndexItem(index: 0, title: "View 0"),
- PagingIndexItem(index: 1, title: "View 1"),
- PagingIndexItem(index: 2, title: "View 2"),
- PagingIndexItem(index: 3, title: "View 3"),
- ]
-
- var body: some View {
- PageView(items: items) { item in
- Text(item.title)
- .font(.largeTitle)
- .foregroundColor(.gray)
- .onTapGesture {
- items = [
- PagingIndexItem(index: 0, title: "View 5"),
- PagingIndexItem(index: 1, title: "View 6"),
- ]
- }
- }
- }
-}
diff --git a/ExampleSwiftUI/ChangeItemsView.swift b/ExampleSwiftUI/ChangeItemsView.swift
new file mode 100644
index 00000000..a2106420
--- /dev/null
+++ b/ExampleSwiftUI/ChangeItemsView.swift
@@ -0,0 +1,61 @@
+import Parchment
+import SwiftUI
+import UIKit
+
+struct ChangeItemsView: View {
+ @State var isToggled: Bool = false
+
+ var body: some View {
+ PageView {
+ if isToggled {
+ Page("Title 2") {
+ VStack {
+ Text("Page 2")
+ .font(.largeTitle)
+ .padding(.bottom)
+
+ Button("Click me") {
+ isToggled.toggle()
+ }
+ }
+ }
+
+ Page("Title 3") {
+ VStack {
+ Text("Page 3")
+ .font(.largeTitle)
+ .padding(.bottom)
+
+ Button("Click me") {
+ isToggled.toggle()
+ }
+ }
+ }
+ } else {
+ Page("Title 0") {
+ VStack {
+ Text("Page 0")
+ .font(.largeTitle)
+ .padding(.bottom)
+
+ Button("Click me") {
+ isToggled.toggle()
+ }
+ }
+ }
+
+ Page("Title 1") {
+ VStack {
+ Text("Page 1")
+ .font(.largeTitle)
+ .padding(.bottom)
+
+ Button("Click me") {
+ isToggled.toggle()
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ExampleSwiftUI/CustomIndicatorView.swift b/ExampleSwiftUI/CustomIndicatorView.swift
new file mode 100644
index 00000000..2867c95f
--- /dev/null
+++ b/ExampleSwiftUI/CustomIndicatorView.swift
@@ -0,0 +1,59 @@
+import Parchment
+import SwiftUI
+import UIKit
+
+struct CustomIndicatorView: View {
+ var body: some View {
+ PageView {
+ Page("Scone") {
+ Text("Scone")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Cinnamon Roll") {
+ Text("Cinnamon Roll")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Croissant") {
+ Text("Croissant")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Muffin") {
+ Text("Muffin")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+ }
+ .borderColor(.black.opacity(0.1))
+ .indicatorOptions(.visible(height: 2))
+ .indicatorStyle(SquigglyIndicatorStyle())
+
+ }
+}
+
+struct SquigglyIndicatorStyle: PagingIndicatorStyle {
+ func makeBody(configuration: Configuration) -> some View {
+ SquigglyShape()
+ .stroke(.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round))
+ }
+}
+
+struct SquigglyShape: Shape {
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: .zero)
+
+ for x in stride(from: 0, through: rect.width, by: 1) {
+ let sine = sin(x / 1.5)
+ let y = rect.height * sine
+ path.addLine(to: CGPoint(x: x, y: y))
+ }
+
+ return path
+ }
+}
diff --git a/ExampleSwiftUI/CustomizedView.swift b/ExampleSwiftUI/CustomizedView.swift
new file mode 100644
index 00000000..57fc8470
--- /dev/null
+++ b/ExampleSwiftUI/CustomizedView.swift
@@ -0,0 +1,45 @@
+import Parchment
+import SwiftUI
+import UIKit
+
+struct CustomizedView: View {
+ var body: some View {
+ PageView {
+ Page("Title 1") {
+ VStack(spacing: 25) {
+ Text("Page 1")
+ Image(systemName: "arrow.down")
+ }
+ .font(.largeTitle)
+ }
+
+ Page("Title 2") {
+ VStack(spacing: 25) {
+ Image(systemName: "arrow.up")
+ Text("Page 2")
+ }
+ .font(.largeTitle)
+ }
+ }
+ .menuItemSize(.fixed(width: 100, height: 60))
+ .menuItemSpacing(20)
+ .menuItemLabelSpacing(30)
+ .selectedColor(.blue)
+ .foregroundColor(.black)
+ .menuBackgroundColor(.white)
+ .backgroundColor(.white)
+ .selectedBackgroundColor(.white)
+ .menuInsets(.vertical, 20)
+ .menuHorizontalAlignment(.center)
+ .menuPosition(.bottom)
+ .menuTransition(.scrollAlongside)
+ .menuInteraction(.swipe)
+ .contentInteraction(.scrolling)
+ .contentNavigationOrientation(.vertical)
+ .selectedScrollPosition(.preferCentered)
+ .indicatorOptions(.visible(height: 4))
+ .indicatorColor(.blue)
+ .borderOptions(.visible(height: 4))
+ .borderColor(.blue.opacity(0.2))
+ }
+}
diff --git a/ExampleSwiftUI/DefaultView.swift b/ExampleSwiftUI/DefaultView.swift
index aed1b870..9226e6b7 100644
--- a/ExampleSwiftUI/DefaultView.swift
+++ b/ExampleSwiftUI/DefaultView.swift
@@ -3,18 +3,52 @@ import SwiftUI
import UIKit
struct DefaultView: View {
- let items = [
- PagingIndexItem(index: 0, title: "View 0"),
- PagingIndexItem(index: 1, title: "View 1"),
- PagingIndexItem(index: 2, title: "View 2"),
- PagingIndexItem(index: 3, title: "View 3"),
- ]
-
var body: some View {
- PageView(items: items) { item in
- Text(item.title)
- .font(.largeTitle)
- .foregroundColor(.gray)
+ PageView {
+ Page { _ in
+ Image(systemName: "star.fill")
+ .padding()
+ } content: {
+ Text("Page 1")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 2") {
+ Text("Page 2")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 3") {
+ Text("Page 3")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 4") {
+ Text("Page 4")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Some very long title") {
+ Text("Page 5")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 6") {
+ Text("Page 6")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 7") {
+ Text("Page 7")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
}
}
}
diff --git a/ExampleSwiftUI/DynamicItemsView.swift b/ExampleSwiftUI/DynamicItemsView.swift
new file mode 100644
index 00000000..a4f14994
--- /dev/null
+++ b/ExampleSwiftUI/DynamicItemsView.swift
@@ -0,0 +1,27 @@
+import Parchment
+import SwiftUI
+import UIKit
+
+struct DynamicItemsView: View {
+ @State var items: [Int] = [0, 1, 2, 3, 4]
+
+ var body: some View {
+ PageView(items, id: \.self) { item in
+ Page("Title \(item)") {
+ VStack {
+ Text("Page \(item)")
+ .font(.largeTitle)
+ .padding(.bottom)
+
+ Button("Click me") {
+ if items.count > 2 {
+ items = [5, 6]
+ } else {
+ items = [0, 1, 2, 3, 4]
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ExampleSwiftUI/ExampleApp.swift b/ExampleSwiftUI/ExampleApp.swift
index 977212c4..232b8b13 100644
--- a/ExampleSwiftUI/ExampleApp.swift
+++ b/ExampleSwiftUI/ExampleApp.swift
@@ -6,10 +6,19 @@ struct ExampleApp: App {
WindowGroup {
NavigationView {
List {
- NavigationLink("Default", destination: DefaultView())
- NavigationLink("Change selected index", destination: SelectedIndexView())
- NavigationLink("Lifecycle events", destination: LifecycleView())
- NavigationLink("Change items", destination: ChangeItemsView())
+ 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.")
+
+ Section {
+ NavigationLink("Default", destination: DefaultView())
+ NavigationLink("Interpolated", destination: InterpolatedView())
+ NavigationLink("Customized", destination: CustomizedView())
+ NavigationLink("Change selected index", destination: SelectedIndexView())
+ NavigationLink("Lifecycle events", destination: LifecycleView())
+ NavigationLink("Change items", destination: ChangeItemsView())
+ NavigationLink("Dynamic items", destination: DynamicItemsView())
+ NavigationLink("Custom indicator", destination: CustomIndicatorView())
+ NavigationLink("Scrolling Views", destination: ScrollingView())
+ }
}
.navigationBarTitleDisplayMode(.inline)
}
diff --git a/ExampleSwiftUI/InterpolatedView.swift b/ExampleSwiftUI/InterpolatedView.swift
new file mode 100644
index 00000000..a50d63cd
--- /dev/null
+++ b/ExampleSwiftUI/InterpolatedView.swift
@@ -0,0 +1,67 @@
+import Parchment
+import SwiftUI
+import UIKit
+
+struct InterpolatedView: View {
+ var body: some View {
+ PageView {
+ Page { state in
+ Image(systemName: "star.fill")
+ .scaleEffect(x: 1 + state.progress, y: 1 + state.progress)
+ .rotationEffect(Angle(degrees: 180 * state.progress))
+ .padding(30 * state.progress + 20)
+ } content: {
+ Text("Page 1")
+ .font(.largeTitle)
+ .foregroundColor(.secondary)
+ }
+ Page { state in
+ Text("Rotate")
+ .fixedSize()
+ .rotationEffect(Angle(degrees: 90 * state.progress))
+ .padding(.horizontal, 10)
+ } content: {
+ Text("Page 2")
+ .font(.largeTitle)
+ .foregroundColor(.secondary)
+ }
+
+ Page { state in
+ Text("Tracking")
+ .tracking(10 * state.progress)
+ .fixedSize()
+ .padding()
+ } content: {
+ Text("Page 3")
+ .font(.largeTitle)
+ .foregroundColor(.secondary)
+ }
+
+ Page { state in
+ Text("Growing")
+ .fixedSize()
+ .padding(.vertical)
+ .padding(.horizontal, 20 * state.progress + 10)
+ .background(Color.black.opacity(0.1))
+ .cornerRadius(6)
+ } content: {
+ Text("Page 4")
+ .font(.largeTitle)
+ .foregroundColor(.secondary)
+ }
+
+ Page("Normal") {
+ Text("Page 5")
+ .font(.largeTitle)
+ .foregroundColor(.secondary)
+ }
+
+ Page("Normal") {
+ Text("Page 6")
+ .font(.largeTitle)
+ .foregroundColor(.secondary)
+ }
+ }
+ .menuItemSize(.selfSizing(estimatedWidth: 100, height: 80))
+ }
+}
diff --git a/ExampleSwiftUI/LifecycleView.swift b/ExampleSwiftUI/LifecycleView.swift
index 3f176556..fa195c5b 100644
--- a/ExampleSwiftUI/LifecycleView.swift
+++ b/ExampleSwiftUI/LifecycleView.swift
@@ -3,27 +3,34 @@ import SwiftUI
import UIKit
struct LifecycleView: View {
- let items = [
- PagingIndexItem(index: 0, title: "View 0"),
- PagingIndexItem(index: 1, title: "View 1"),
- PagingIndexItem(index: 2, title: "View 2"),
- PagingIndexItem(index: 3, title: "View 3"),
- ]
-
var body: some View {
- PageView(items: items) { item in
- Text(item.title)
- .font(.largeTitle)
- .foregroundColor(.gray)
+ PageView {
+ Page("Title 1") {
+ Text("Page 1")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 2") {
+ Text("Page 2")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 3") {
+ Text("Page 3")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
}
- .willScroll { pagingItem in
- print("will scroll: ", pagingItem)
+ .willScroll { item in
+ print("will scroll: ", item)
}
- .didScroll { pagingItem in
- print("did scroll: ", pagingItem)
+ .didScroll { item in
+ print("did scroll: ", item)
}
- .didSelect { pagingItem in
- print("did select: ", pagingItem)
+ .didSelect { item in
+ print("did select: ", item)
}
}
}
diff --git a/ExampleSwiftUI/ScrollingView.swift b/ExampleSwiftUI/ScrollingView.swift
new file mode 100644
index 00000000..00e66986
--- /dev/null
+++ b/ExampleSwiftUI/ScrollingView.swift
@@ -0,0 +1,31 @@
+import Parchment
+import SwiftUI
+import UIKit
+
+struct ScrollingView: View {
+ var body: some View {
+ PageView {
+ Page("First") {
+ ScrollingContentView()
+ }
+ Page("Second") {
+ ScrollingContentView()
+ }
+ Page("Third") {
+ ScrollingContentView()
+ }
+ }
+ }
+}
+
+struct ScrollingContentView: View {
+ var body: some View {
+ List {
+ ForEach(0...50 , id: \.self) { item in
+ NavigationLink(destination: Text("\(item)")) {
+ Text("\(item)")
+ }
+ }
+ }
+ }
+}
diff --git a/ExampleSwiftUI/SelectedIndexView.swift b/ExampleSwiftUI/SelectedIndexView.swift
index ec5f17b9..b081698a 100644
--- a/ExampleSwiftUI/SelectedIndexView.swift
+++ b/ExampleSwiftUI/SelectedIndexView.swift
@@ -3,22 +3,39 @@ import SwiftUI
import UIKit
struct SelectedIndexView: View {
- var items = [
- PagingIndexItem(index: 0, title: "View 0"),
- PagingIndexItem(index: 1, title: "View 1"),
- PagingIndexItem(index: 2, title: "View 2"),
- PagingIndexItem(index: 3, title: "View 3"),
- ]
- @State var selectedIndex: Int = 2
+ @State var selectedIndex: Int = 0
var body: some View {
- PageView(items: items, selectedIndex: $selectedIndex) { item in
- Text(item.title)
- .font(.largeTitle)
- .foregroundColor(.gray)
- .onTapGesture {
- selectedIndex = 0
+ PageView(selectedIndex: $selectedIndex) {
+ Page("Title 0") {
+ VStack {
+ Text("Page 0")
+ .font(.largeTitle)
+ .padding(.bottom)
+
+ Button("Click me") {
+ selectedIndex = 2
+ }
+ }
+ }
+
+ Page("Title 1") {
+ Text("Page 1")
+ .font(.largeTitle)
+ .foregroundColor(.gray)
+ }
+
+ Page("Title 2") {
+ VStack {
+ Text("Page 2")
+ .font(.largeTitle)
+ .padding(.bottom)
+
+ Button("Click me") {
+ selectedIndex = 0
+ }
}
+ }
}
}
}
diff --git a/Parchment.xcodeproj/project.pbxproj b/Parchment.xcodeproj/project.pbxproj
index b0cffeaf..8b1bff8f 100644
--- a/Parchment.xcodeproj/project.pbxproj
+++ b/Parchment.xcodeproj/project.pbxproj
@@ -39,11 +39,18 @@
3EA04A641C53BFF40054E5E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EA04A631C53BFF40054E5E0 /* Assets.xcassets */; };
3EA04A671C53BFF40054E5E0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EA04A651C53BFF40054E5E0 /* LaunchScreen.storyboard */; };
3EFEFBF71C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */; };
+ 95074D0E29E05FEE003FA973 /* WheelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95074D0C29E05F62003FA973 /* WheelViewController.swift */; };
+ 9509917129DED89E001BAA09 /* PagingViewControllerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9509917029DED89E001BAA09 /* PagingViewControllerDelegateTests.swift */; };
950ABE422437BD4D00CAD458 /* PagingNavigationOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */; };
950ABE452438975300CAD458 /* PageViewExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950ABE442438975300CAD458 /* PageViewExampleViewController.swift */; };
951E163720A21D3A0055E9D4 /* PagingViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954842601F4251F90072038C /* PagingViewControllerTests.swift */; };
952D802F1E37CC09003DCB18 /* PagingTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952D802E1E37CC09003DCB18 /* PagingTransition.swift */; };
+ 9530E25329DEC2E5004FC88C /* PageContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9530E25229DEC2E5004FC88C /* PageContentConfiguration.swift */; };
953B8D352416C3DC0047BBA1 /* SelfSizingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953B8D342416C3DC0047BBA1 /* SelfSizingViewController.swift */; };
+ 95428A562B80F6EA00D61143 /* ScrollingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95428A552B80F6EA00D61143 /* ScrollingView.swift */; };
+ 9546B2AB2A1D2F06000390C6 /* PagingHostingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9546B2AA2A1D2F06000390C6 /* PagingHostingIndicatorView.swift */; };
+ 9546B2AD2A1D44EF000390C6 /* CustomIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9546B2AC2A1D44EF000390C6 /* CustomIndicatorView.swift */; };
+ 9546B2AF2A1D4767000390C6 /* PagingIndicatorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9546B2AE2A1D4767000390C6 /* PagingIndicatorStyle.swift */; };
954842591F42438E0072038C /* PagingInvalidationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954842581F42438E0072038C /* PagingInvalidationContext.swift */; };
9548425D1F42486B0072038C /* PagingDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9548425C1F42486B0072038C /* PagingDiff.swift */; };
954842631F4252070072038C /* PagingDiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954842621F4252070072038C /* PagingDiffTests.swift */; };
@@ -85,6 +92,16 @@
9566F567257983B200CCA8FC /* Resources.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9566F566257983B200CCA8FC /* Resources.xcassets */; };
9568922B222C525C00AFF250 /* CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9568922A222C525C00AFF250 /* CollectionView.swift */; };
956EBE5E248BC426003ED4BA /* PagingCollectionViewLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956EBE5D248BC426003ED4BA /* PagingCollectionViewLayoutTests.swift */; };
+ 956F000A29CCFC6C00477E94 /* InterpolatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956F000929CCFC6C00477E94 /* InterpolatedView.swift */; };
+ 956F000C29D872C400477E94 /* PageState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956F000B29D872C400477E94 /* PageState.swift */; };
+ 956FFFC429BD6E6400477E94 /* PageItemBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFC329BD6E6400477E94 /* PageItemBuilder.swift */; };
+ 956FFFC629BD786800477E94 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFC529BD786800477E94 /* Page.swift */; };
+ 956FFFC829BD792100477E94 /* PageItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFC729BD792100477E94 /* PageItemCell.swift */; };
+ 956FFFCA29BE090800477E94 /* PageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFC929BE090800477E94 /* PageItem.swift */; };
+ 956FFFCC29BE1FD100477E94 /* ChangeItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFCB29BE1FD100477E94 /* ChangeItemsView.swift */; };
+ 956FFFCE29BE235200477E94 /* PagingControllerRepresentableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFCD29BE235200477E94 /* PagingControllerRepresentableView.swift */; };
+ 956FFFD029BE23F900477E94 /* PageViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFCF29BE23F900477E94 /* PageViewCoordinator.swift */; };
+ 956FFFD229BE273B00477E94 /* CustomizedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956FFFD129BE273B00477E94 /* CustomizedView.swift */; };
9575BEB32461FEF9002403F6 /* CreateDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9575BEB22461FEF9002403F6 /* CreateDistance.swift */; };
9575BEB52462034B002403F6 /* PagingDistanceRightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9575BEB42462034B002403F6 /* PagingDistanceRightTests.swift */; };
9575BEB72463490B002403F6 /* PagingDistanceCenteredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9575BEB62463490B002403F6 /* PagingDistanceCenteredTests.swift */; };
@@ -130,13 +147,12 @@
95F5660F2128707900F2A75E /* PagingMenuItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D1BE1D211E409400E86B72 /* PagingMenuItemSource.swift */; };
95F83D6426237D2B003B728F /* SelectedIndexView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F83D6326237D2B003B728F /* SelectedIndexView.swift */; };
95F83D6C26237DA4003B728F /* LifecycleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F83D6B26237DA4003B728F /* LifecycleView.swift */; };
- 95F83D832623804F003B728F /* ChangeItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F83D822623804F003B728F /* ChangeItems.swift */; };
+ 95F83D832623804F003B728F /* DynamicItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F83D822623804F003B728F /* DynamicItemsView.swift */; };
95FE3AF91FFEDBCE00E6F2AD /* PagingDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FE3AF81FFEDBCE00E6F2AD /* PagingDistance.swift */; };
95FEEA4524215FCA009B5B64 /* PageViewManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA4424215FCA009B5B64 /* PageViewManagerTests.swift */; };
95FEEA4D2423C44A009B5B64 /* MockPageViewManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA4C2423C44A009B5B64 /* MockPageViewManagerDelegate.swift */; };
95FEEA4F2423F213009B5B64 /* PageViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA4E2423F213009B5B64 /* PageViewManager.swift */; };
95FEEA512423F752009B5B64 /* MockPageViewManagerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA502423F752009B5B64 /* MockPageViewManagerDataSource.swift */; };
- DAD34A632BA5B08900069395 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = DAD34A622BA5B08900069395 /* README.md */; };
E8CB720A23D70E1400A9B089 /* PagingMenuPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8CB720923D70E1400A9B089 /* PagingMenuPosition.swift */; };
/* End PBXBuildFile section */
@@ -247,10 +263,17 @@
3EA04A681C53BFF40054E5E0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
3EC0184C1C95F993005421AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Resources/Info.plist; sourceTree = ""; };
3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingIndicatorLayoutAttributesTests.swift; sourceTree = ""; };
+ 95074D0C29E05F62003FA973 /* WheelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelViewController.swift; sourceTree = ""; };
+ 9509917029DED89E001BAA09 /* PagingViewControllerDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingViewControllerDelegateTests.swift; sourceTree = ""; };
950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingNavigationOrientation.swift; sourceTree = ""; };
950ABE442438975300CAD458 /* PageViewExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewExampleViewController.swift; sourceTree = ""; };
952D802E1E37CC09003DCB18 /* PagingTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingTransition.swift; sourceTree = ""; };
+ 9530E25229DEC2E5004FC88C /* PageContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentConfiguration.swift; sourceTree = ""; };
953B8D342416C3DC0047BBA1 /* SelfSizingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingViewController.swift; sourceTree = ""; };
+ 95428A552B80F6EA00D61143 /* ScrollingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingView.swift; sourceTree = ""; };
+ 9546B2AA2A1D2F06000390C6 /* PagingHostingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingHostingIndicatorView.swift; sourceTree = ""; };
+ 9546B2AC2A1D44EF000390C6 /* CustomIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomIndicatorView.swift; sourceTree = ""; };
+ 9546B2AE2A1D4767000390C6 /* PagingIndicatorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingIndicatorStyle.swift; sourceTree = ""; };
954842581F42438E0072038C /* PagingInvalidationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingInvalidationContext.swift; sourceTree = ""; };
9548425C1F42486B0072038C /* PagingDiff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingDiff.swift; sourceTree = ""; };
954842601F4251F90072038C /* PagingViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingViewControllerTests.swift; sourceTree = ""; };
@@ -282,6 +305,16 @@
9566F566257983B200CCA8FC /* Resources.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Resources.xcassets; sourceTree = ""; };
9568922A222C525C00AFF250 /* CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionView.swift; sourceTree = ""; };
956EBE5D248BC426003ED4BA /* PagingCollectionViewLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingCollectionViewLayoutTests.swift; sourceTree = ""; };
+ 956F000929CCFC6C00477E94 /* InterpolatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpolatedView.swift; sourceTree = ""; };
+ 956F000B29D872C400477E94 /* PageState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageState.swift; sourceTree = ""; };
+ 956FFFC329BD6E6400477E94 /* PageItemBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageItemBuilder.swift; sourceTree = ""; };
+ 956FFFC529BD786800477E94 /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; };
+ 956FFFC729BD792100477E94 /* PageItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageItemCell.swift; sourceTree = ""; };
+ 956FFFC929BE090800477E94 /* PageItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageItem.swift; sourceTree = ""; };
+ 956FFFCB29BE1FD100477E94 /* ChangeItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeItemsView.swift; sourceTree = ""; };
+ 956FFFCD29BE235200477E94 /* PagingControllerRepresentableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingControllerRepresentableView.swift; sourceTree = ""; };
+ 956FFFCF29BE23F900477E94 /* PageViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewCoordinator.swift; sourceTree = ""; };
+ 956FFFD129BE273B00477E94 /* CustomizedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizedView.swift; sourceTree = ""; };
9575BEB22461FEF9002403F6 /* CreateDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDistance.swift; sourceTree = ""; };
9575BEB42462034B002403F6 /* PagingDistanceRightTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingDistanceRightTests.swift; sourceTree = ""; };
9575BEB62463490B002403F6 /* PagingDistanceCenteredTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingDistanceCenteredTests.swift; sourceTree = ""; };
@@ -331,13 +364,12 @@
95ED82632B5056AC0055227B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
95F83D6326237D2B003B728F /* SelectedIndexView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedIndexView.swift; sourceTree = ""; };
95F83D6B26237DA4003B728F /* LifecycleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleView.swift; sourceTree = ""; };
- 95F83D822623804F003B728F /* ChangeItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeItems.swift; sourceTree = ""; };
+ 95F83D822623804F003B728F /* DynamicItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicItemsView.swift; sourceTree = ""; };
95FE3AF81FFEDBCE00E6F2AD /* PagingDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingDistance.swift; sourceTree = ""; };
95FEEA4424215FCA009B5B64 /* PageViewManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewManagerTests.swift; sourceTree = ""; };
95FEEA4C2423C44A009B5B64 /* MockPageViewManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPageViewManagerDelegate.swift; sourceTree = ""; };
95FEEA4E2423F213009B5B64 /* PageViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewManager.swift; sourceTree = ""; };
95FEEA502423F752009B5B64 /* MockPageViewManagerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPageViewManagerDataSource.swift; sourceTree = ""; };
- DAD34A622BA5B08900069395 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
E8CB720923D70E1400A9B089 /* PagingMenuPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingMenuPosition.swift; sourceTree = ""; };
E9D1BE1D211E409400E86B72 /* PagingMenuItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingMenuItemSource.swift; sourceTree = ""; };
/* End PBXFileReference section */
@@ -419,6 +451,13 @@
3E4283FB1C99CF9000032D95 /* PagingItems.swift */,
952D802E1E37CC09003DCB18 /* PagingTransition.swift */,
950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */,
+ 956FFFC329BD6E6400477E94 /* PageItemBuilder.swift */,
+ 956FFFC529BD786800477E94 /* Page.swift */,
+ 956F000B29D872C400477E94 /* PageState.swift */,
+ 956FFFC729BD792100477E94 /* PageItemCell.swift */,
+ 956FFFC929BE090800477E94 /* PageItem.swift */,
+ 956FFFCD29BE235200477E94 /* PagingControllerRepresentableView.swift */,
+ 9530E25229DEC2E5004FC88C /* PageContentConfiguration.swift */,
);
path = Structs;
sourceTree = "";
@@ -426,6 +465,9 @@
3E4090A31C88BD1700800E22 /* Classes */ = {
isa = PBXGroup;
children = (
+ 955453C32413C80F00923BC8 /* PageViewController.swift */,
+ 956FFFCF29BE23F900477E94 /* PageViewCoordinator.swift */,
+ 95FEEA4E2423F213009B5B64 /* PageViewManager.swift */,
3E4090A91C88BDD100800E22 /* PagingBorderLayoutAttributes.swift */,
3E49C71F1C8F5C13006269DD /* PagingBorderView.swift */,
3E49C7201C8F5C13006269DD /* PagingCell.swift */,
@@ -433,6 +475,7 @@
3E4090AA1C88BDD100800E22 /* PagingCollectionViewLayout.swift */,
95591F22222C522800677B4B /* PagingController.swift */,
95E4BA711FF15EFE008871A3 /* PagingFiniteDataSource.swift */,
+ 9546B2AA2A1D2F06000390C6 /* PagingHostingIndicatorView.swift */,
3E4090AC1C88BDD100800E22 /* PagingIndicatorLayoutAttributes.swift */,
3E49C7221C8F5C13006269DD /* PagingIndicatorView.swift */,
954842581F42438E0072038C /* PagingInvalidationContext.swift */,
@@ -443,8 +486,6 @@
3E562ACD1CE7CD8C007623B3 /* PagingTitleCell.swift */,
3E49C7231C8F5C13006269DD /* PagingView.swift */,
3E49C7241C8F5C13006269DD /* PagingViewController.swift */,
- 955453C32413C80F00923BC8 /* PageViewController.swift */,
- 95FEEA4E2423F213009B5B64 /* PageViewManager.swift */,
);
path = Classes;
sourceTree = "";
@@ -465,6 +506,7 @@
3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */,
3E5E93E21CE7D899000762A1 /* PagingStateTests.swift */,
954842601F4251F90072038C /* PagingViewControllerTests.swift */,
+ 9509917029DED89E001BAA09 /* PagingViewControllerDelegateTests.swift */,
955D02432434E56900416E71 /* PagingViewTests.swift */,
9566F55E257982B500CCA8FC /* UIColorInterpolationTests.swift */,
95D78FE5228728FD00E6EE7C /* Mocks */,
@@ -477,7 +519,6 @@
3EA04A411C53BFE40054E5E0 = {
isa = PBXGroup;
children = (
- DAD34A622BA5B08900069395 /* README.md */,
3EA04A4D1C53BFE40054E5E0 /* Parchment */,
3E504EC51C7465B000AE1CE3 /* ParchmentTests */,
95D2AE42242BC1FA00AC3D46 /* ParchmentUITests */,
@@ -553,10 +594,19 @@
3E4189201C9573FA001E0284 /* PagingViewControllerInfiniteDataSource.swift */,
95D7900F2299CE6100E6EE7C /* PagingViewControllerSizeDelegate.swift */,
95868C33200412DE004B392B /* Tween.swift */,
+ 9546B2AE2A1D4767000390C6 /* PagingIndicatorStyle.swift */,
);
path = Protocols;
sourceTree = "";
};
+ 95074D0A29E05F62003FA973 /* Wheel */ = {
+ isa = PBXGroup;
+ children = (
+ 95074D0C29E05F62003FA973 /* WheelViewController.swift */,
+ );
+ path = Wheel;
+ sourceTree = "";
+ };
950ABE432438972400CAD458 /* PageViewController */ = {
isa = PBXGroup;
children = (
@@ -595,6 +645,7 @@
955453CD2413DC5A00923BC8 /* Calendar */,
953B8D332416C3C60047BBA1 /* SelfSizing */,
955453D42413DD7A00923BC8 /* SizeDelegate */,
+ 95074D0A29E05F62003FA973 /* Wheel */,
955453D72413E0FA00923BC8 /* Images */,
955453DC2413E1A500923BC8 /* Icons */,
955453F12415180A00923BC8 /* Storyboard */,
@@ -723,14 +774,19 @@
95D2AE50242BCC9500AC3D46 /* ExampleSwiftUI */ = {
isa = PBXGroup;
children = (
+ 95D2AE5F242BCC9900AC3D46 /* Info.plist */,
95D2AE51242BCC9500AC3D46 /* ExampleApp.swift */,
95D2AE55242BCC9500AC3D46 /* DefaultView.swift */,
- 95F83D6326237D2B003B728F /* SelectedIndexView.swift */,
+ 956FFFCB29BE1FD100477E94 /* ChangeItemsView.swift */,
+ 95428A552B80F6EA00D61143 /* ScrollingView.swift */,
+ 956FFFD129BE273B00477E94 /* CustomizedView.swift */,
+ 95F83D822623804F003B728F /* DynamicItemsView.swift */,
+ 956F000929CCFC6C00477E94 /* InterpolatedView.swift */,
95F83D6B26237DA4003B728F /* LifecycleView.swift */,
- 95F83D822623804F003B728F /* ChangeItems.swift */,
+ 95F83D6326237D2B003B728F /* SelectedIndexView.swift */,
+ 9546B2AC2A1D44EF000390C6 /* CustomIndicatorView.swift */,
95D2AE57242BCC9900AC3D46 /* Assets.xcassets */,
95D2AE5C242BCC9900AC3D46 /* LaunchScreen.storyboard */,
- 95D2AE5F242BCC9900AC3D46 /* Info.plist */,
95D2AE59242BCC9900AC3D46 /* Preview Content */,
);
path = ExampleSwiftUI;
@@ -881,7 +937,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1130;
- LastUpgradeCheck = 1530;
+ LastUpgradeCheck = 1540;
ORGANIZATIONNAME = "Martin Rechsteiner";
TargetAttributes = {
3E504EC31C7465B000AE1CE3 = {
@@ -947,7 +1003,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- DAD34A632BA5B08900069395 /* README.md in Resources */,
95ED82642B5056AC0055227B /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1004,6 +1059,7 @@
95D78FDB2287138F00E6EE7C /* MockCollectionView.swift in Sources */,
9575BEB72463490B002403F6 /* PagingDistanceCenteredTests.swift in Sources */,
9566F55F257982B500CCA8FC /* UIColorInterpolationTests.swift in Sources */,
+ 9509917129DED89E001BAA09 /* PagingViewControllerDelegateTests.swift in Sources */,
954E7DEE1F48AE6E00342ECF /* Item.swift in Sources */,
954842631F4252070072038C /* PagingDiffTests.swift in Sources */,
95D78FDD228713B800E6EE7C /* MockCollectionViewLayout.swift in Sources */,
@@ -1029,6 +1085,7 @@
9597F2951E3903F4003FD289 /* UIColor+interpolation.swift in Sources */,
95A0AF001FF707910043B90A /* PagingIndexItem.swift in Sources */,
3E49C72A1C8F5C13006269DD /* PagingViewController.swift in Sources */,
+ 956FFFCA29BE090800477E94 /* PageItem.swift in Sources */,
950ABE422437BD4D00CAD458 /* PagingNavigationOrientation.swift in Sources */,
95A84B0D20ED46920031520F /* AnyPagingItem.swift in Sources */,
95D790102299CE6100E6EE7C /* PagingViewControllerSizeDelegate.swift in Sources */,
@@ -1038,6 +1095,7 @@
3E2AAD281CA831AB0044AAA5 /* UIView+constraints.swift in Sources */,
95D2AE6F242EA3EF00AC3D46 /* PageViewControllerDelegate.swift in Sources */,
95A84B0520E586FA0031520F /* PagingStaticDataSource.swift in Sources */,
+ 9530E25329DEC2E5004FC88C /* PageContentConfiguration.swift in Sources */,
3E49C7251C8F5C13006269DD /* PagingBorderView.swift in Sources */,
E8CB720A23D70E1400A9B089 /* PagingMenuPosition.swift in Sources */,
95E4BA721FF15EFE008871A3 /* PagingFiniteDataSource.swift in Sources */,
@@ -1048,6 +1106,8 @@
955453C42413C80F00923BC8 /* PageViewController.swift in Sources */,
3E4090A21C88BD0A00800E22 /* PagingIndicatorMetric.swift in Sources */,
95D2AE71242EA40A00AC3D46 /* PageViewControllerDataSource.swift in Sources */,
+ 956FFFC629BD786800477E94 /* Page.swift in Sources */,
+ 956F000C29D872C400477E94 /* PageState.swift in Sources */,
955444BE1FC9CCEC001EC26B /* PagingSelectedScrollPosition.swift in Sources */,
3E4189211C9573FA001E0284 /* PagingViewControllerInfiniteDataSource.swift in Sources */,
95868C32200412D8004B392B /* InvalidationState.swift in Sources */,
@@ -1066,15 +1126,21 @@
3E49C7281C8F5C13006269DD /* PagingIndicatorView.swift in Sources */,
95D2AE36242BB22F00AC3D46 /* PageViewDirection.swift in Sources */,
95B301171E59FCD500B95D02 /* UIEdgeInsets.swift in Sources */,
+ 9546B2AB2A1D2F06000390C6 /* PagingHostingIndicatorView.swift in Sources */,
955444C01FC9CCFF001EC26B /* PagingMenuHorizontalAlignment.swift in Sources */,
+ 9546B2AF2A1D4767000390C6 /* PagingIndicatorStyle.swift in Sources */,
3E49C7261C8F5C13006269DD /* PagingCell.swift in Sources */,
95D790162299D56300E6EE7C /* PagingMenuDataSource.swift in Sources */,
+ 956FFFC429BD6E6400477E94 /* PageItemBuilder.swift in Sources */,
955444BA1FC9CCBF001EC26B /* PagingIndicatorOptions.swift in Sources */,
95D790182299D57A00E6EE7C /* PagingMenuDelegate.swift in Sources */,
+ 956FFFC829BD792100477E94 /* PageItemCell.swift in Sources */,
3E49C72E1C8F5CCE006269DD /* PagingCellViewModel.swift in Sources */,
95868C2C2003DA87004B392B /* PagingContentInteraction.swift in Sources */,
955444B81FC9CCA6001EC26B /* PagingMenuItemSize.swift in Sources */,
+ 956FFFCE29BE235200477E94 /* PagingControllerRepresentableView.swift in Sources */,
3E562ACE1CE7CD8C007623B3 /* PagingTitleCell.swift in Sources */,
+ 956FFFD029BE23F900477E94 /* PageViewCoordinator.swift in Sources */,
95FE3AF91FFEDBCE00E6F2AD /* PagingDistance.swift in Sources */,
95D2AE38242BB24800AC3D46 /* PageViewState.swift in Sources */,
);
@@ -1107,6 +1173,7 @@
955453E52413E37700923BC8 /* TableViewController.swift in Sources */,
955453D02413DCB700923BC8 /* CalendarViewController.swift in Sources */,
955453D32413DCB700923BC8 /* DateFormatters.swift in Sources */,
+ 95074D0E29E05FEE003FA973 /* WheelViewController.swift in Sources */,
3EA04A5D1C53BFF40054E5E0 /* AppDelegate.swift in Sources */,
955453DE2413E1B200923BC8 /* IconPagingCell.swift in Sources */,
3E2AAD201CA81EA20044AAA5 /* ContentViewController.swift in Sources */,
@@ -1125,11 +1192,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 95F83D832623804F003B728F /* ChangeItems.swift in Sources */,
+ 95F83D832623804F003B728F /* DynamicItemsView.swift in Sources */,
95F83D6C26237DA4003B728F /* LifecycleView.swift in Sources */,
+ 956FFFD229BE273B00477E94 /* CustomizedView.swift in Sources */,
95D2AE52242BCC9500AC3D46 /* ExampleApp.swift in Sources */,
+ 956F000A29CCFC6C00477E94 /* InterpolatedView.swift in Sources */,
+ 95428A562B80F6EA00D61143 /* ScrollingView.swift in Sources */,
+ 956FFFCC29BE1FD100477E94 /* ChangeItemsView.swift in Sources */,
95F83D6426237D2B003B728F /* SelectedIndexView.swift in Sources */,
95D2AE56242BCC9500AC3D46 /* DefaultView.swift in Sources */,
+ 9546B2AD2A1D44EF000390C6 /* CustomIndicatorView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1229,7 +1301,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
@@ -1291,7 +1362,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
diff --git a/Parchment.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Parchment.xcodeproj/xcshareddata/xcschemes/Example.xcscheme
index 72f4036e..9e221c8d 100644
--- a/Parchment.xcodeproj/xcshareddata/xcschemes/Example.xcscheme
+++ b/Parchment.xcodeproj/xcshareddata/xcschemes/Example.xcscheme
@@ -1,6 +1,6 @@
{
+ weak var value: T?
+
+ init(value: T) {
+ self.value = value
+ }
+ }
+
+ var parent: PagingControllerRepresentableView
+ var controllers: [Int: WeakReference] = [:]
+
+ init(_ pagingController: PagingControllerRepresentableView) {
+ parent = pagingController
+ }
+
+ func numberOfViewControllers(in _: PagingViewController) -> Int {
+ return parent.items.count
+ }
+
+ func pagingViewController(
+ _: PagingViewController,
+ viewControllerAt index: Int
+ ) -> UIViewController {
+ let item = parent.items[index]
+ let hostingViewController: UIViewController
+
+ if let controller = controllers[item.identifier]?.value {
+ hostingViewController = controller
+ } else {
+ let controller = hostingController(for: item)
+ controllers[item.identifier] = WeakReference(value: controller)
+ hostingViewController = controller
+ }
+
+ let backgroundColor = parent.options.pagingContentBackgroundColor
+ hostingViewController.view.backgroundColor = backgroundColor
+ return hostingViewController
+ }
+
+ func pagingViewController(
+ _: PagingViewController,
+ pagingItemAt index: Int
+ ) -> PagingItem {
+ parent.items[index]
+ }
+
+ func pagingViewController(
+ _ controller: PagingViewController,
+ didScrollToItem pagingItem: PagingItem,
+ startingViewController _: UIViewController?,
+ destinationViewController _: UIViewController,
+ transitionSuccessful _: Bool
+ ) {
+ if let item = pagingItem as? PageItem,
+ let index = parent.items.firstIndex(where: { $0.isEqual(to: item) }) {
+ parent.selectedIndex = index
+ }
+
+ parent.onDidScroll?(pagingItem)
+ }
+
+ func pagingViewController(
+ _: PagingViewController,
+ willScrollToItem pagingItem: PagingItem,
+ startingViewController _: UIViewController,
+ destinationViewController _: UIViewController
+ ) {
+ parent.onWillScroll?(pagingItem)
+ }
+
+ func pagingViewController(
+ _: PagingViewController,
+ didSelectItem pagingItem: PagingItem
+ ) {
+ parent.onDidSelect?(pagingItem)
+ }
+
+ private func hostingController(for pagingItem: PagingItem) -> UIViewController {
+ var hostingViewController: UIViewController
+ if let item = pagingItem as? PageItem {
+ hostingViewController = item.page.content()
+ } else {
+ assertionFailure("""
+ PageItem is required when using the SwiftUI wrappers.
+ Please report if you somehow ended up here.
+ """)
+ hostingViewController = UIViewController()
+ }
+ return hostingViewController
+ }
+}
diff --git a/Parchment/Classes/PageViewManager.swift b/Parchment/Classes/PageViewManager.swift
index 8d2f3681..769e4728 100644
--- a/Parchment/Classes/PageViewManager.swift
+++ b/Parchment/Classes/PageViewManager.swift
@@ -154,8 +154,8 @@ final class PageViewManager {
}
}
- func viewWillLayoutSubviews() {
- layoutsViews()
+ func viewDidLayoutSubviews() {
+ layoutsViews(keepContentOffset: false)
}
func viewWillAppear(_ animated: Bool) {
@@ -170,7 +170,7 @@ final class PageViewManager {
switch state {
case .center, .first, .last, .single:
- layoutsViews()
+ layoutsViews(keepContentOffset: false)
case .empty:
break
}
@@ -360,53 +360,25 @@ final class PageViewManager {
}
private func onScroll(progress: CGFloat) {
- // This means we are overshooting, so we need to continue
- // reporting the old view controllers.
- if didReload {
- switch initialDirection {
- case .forward:
- if let previousViewController = previousViewController,
- let selectedViewController = selectedViewController {
- delegate?.isScrolling(
- from: previousViewController,
- to: selectedViewController,
- progress: progress
- )
- }
- case .reverse:
- if let nextViewController = nextViewController,
- let selectedViewController = selectedViewController {
- delegate?.isScrolling(
- from: nextViewController,
- to: selectedViewController,
- progress: progress
- )
- }
- case .none:
- break
+ switch initialDirection {
+ case .forward:
+ if let selectedViewController = selectedViewController {
+ delegate?.isScrolling(
+ from: selectedViewController,
+ to: nextViewController,
+ progress: progress
+ )
}
- } else {
- // Report progress as normally
- switch initialDirection {
- case .forward:
- if let selectedViewController = selectedViewController {
- delegate?.isScrolling(
- from: selectedViewController,
- to: nextViewController,
- progress: progress
- )
- }
- case .reverse:
- if let selectedViewController = selectedViewController {
- delegate?.isScrolling(
- from: selectedViewController,
- to: previousViewController,
- progress: progress
- )
- }
- case .none:
- break
+ case .reverse:
+ if let selectedViewController = selectedViewController {
+ delegate?.isScrolling(
+ from: selectedViewController,
+ to: previousViewController,
+ progress: progress
+ )
}
+ case .none:
+ break
}
}
diff --git a/Parchment/Classes/PagingCollectionViewLayout.swift b/Parchment/Classes/PagingCollectionViewLayout.swift
index 41e002ca..68ccd359 100644
--- a/Parchment/Classes/PagingCollectionViewLayout.swift
+++ b/Parchment/Classes/PagingCollectionViewLayout.swift
@@ -164,8 +164,8 @@ open class PagingCollectionViewLayout: UICollectionViewLayout, PagingLayout {
// preferred width for each cell. The preferred size is based on
// the layout constraints in each cell.
case .selfSizing where originalAttributes is PagingCellLayoutAttributes:
- if preferredAttributes.frame.width != originalAttributes.frame.width {
- let pagingItem = visibleItems.pagingItem(for: originalAttributes.indexPath)
+ let pagingItem = visibleItems.pagingItem(for: originalAttributes.indexPath)
+ if preferredAttributes.frame.width != preferredSizeCache[pagingItem.identifier] {
preferredSizeCache[pagingItem.identifier] = preferredAttributes.frame.width
return true
}
diff --git a/Parchment/Classes/PagingController.swift b/Parchment/Classes/PagingController.swift
index 31d39695..fb0ea6a1 100644
--- a/Parchment/Classes/PagingController.swift
+++ b/Parchment/Classes/PagingController.swift
@@ -4,6 +4,7 @@ protocol PagingControllerSizeDelegate: AnyObject {
func width(for: PagingItem, isSelected: Bool) -> CGFloat
}
+@MainActor
final class PagingController: NSObject {
weak var dataSource: PagingMenuDataSource?
weak var sizeDelegate: PagingControllerSizeDelegate?
@@ -327,6 +328,24 @@ final class PagingController: NSObject {
}
}
}
+
+ if collectionView.isDragging, case .wheel = options.menuInteraction {
+ let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
+ if let indexPath = collectionView.indexPathForItem(at: center),
+ let currentPagingItem = state.currentPagingItem {
+ let currentIndexPath = visibleItems.indexPath(for: currentPagingItem)
+ if indexPath != currentIndexPath {
+ let pagingItem = visibleItems.pagingItem(for: indexPath)
+ state = .selected(pagingItem: pagingItem)
+ collectionViewLayout.invalidateLayout()
+ delegate?.selectContent(
+ pagingItem: pagingItem,
+ direction: .none,
+ animated: false
+ )
+ }
+ }
+ }
}
// MARK: Private
@@ -367,7 +386,7 @@ final class PagingController: NSObject {
collectionView.alwaysBounceHorizontal = false
switch options.menuInteraction {
- case .scrolling:
+ case .scrolling, .wheel:
collectionView.isScrollEnabled = true
collectionView.alwaysBounceHorizontal = true
case .swipe:
@@ -628,11 +647,28 @@ final class PagingController: NSObject {
}
}
- private func configureSizeCache(for _: PagingItem) {
- if sizeDelegate != nil {
- sizeCache.implementsSizeDelegate = true
- sizeCache.sizeForPagingItem = { [weak self] item, selected in
- self?.sizeDelegate?.width(for: item, isSelected: selected)
+ private func configureSizeCache(for pagingItem: PagingItem) {
+ switch options.menuItemSize {
+ case .selfSizing:
+ if #available(iOS 14.0, *), pagingItem is PageItem {
+ sizeCache.implementsSizeDelegate = true
+ sizeCache.sizeForPagingItem = { [weak self] item, selected in
+ guard let self else { return nil }
+ let item = item as! PageItem
+ let state = PageState(progress: selected ? 1 : 0, isSelected: selected)
+ let configuration = item.page.header(self.options, state)
+ let contentView = configuration.makeContentView()
+ let size = contentView.sizeThatFits(UIView.layoutFittingCompressedSize)
+ return size.width
+ }
+ }
+
+ case .fixed, .sizeToFit:
+ if sizeDelegate != nil {
+ sizeCache.implementsSizeDelegate = true
+ sizeCache.sizeForPagingItem = { [weak self] item, selected in
+ return self?.sizeDelegate?.width(for: item, isSelected: selected)
+ }
}
}
}
@@ -653,8 +689,17 @@ extension PagingController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let pagingItem = visibleItems.items[indexPath.item]
+ var reuseIdentifier: String
+
+ if #available(iOS 14.0, *),
+ let item = pagingItem as? PageItem {
+ reuseIdentifier = item.page.reuseIdentifier
+ } else {
+ reuseIdentifier = String(describing: type(of: pagingItem))
+ }
+
let cell = collectionView.dequeueReusableCell(
- withReuseIdentifier: String(describing: type(of: pagingItem)),
+ withReuseIdentifier: reuseIdentifier,
for: indexPath
) as! PagingCell
var selected: Bool = false
diff --git a/Parchment/Classes/PagingHostingIndicatorView.swift b/Parchment/Classes/PagingHostingIndicatorView.swift
new file mode 100644
index 00000000..cdb9a527
--- /dev/null
+++ b/Parchment/Classes/PagingHostingIndicatorView.swift
@@ -0,0 +1,63 @@
+import UIKit
+import SwiftUI
+
+/// A custom `UICollectionViewReusableView` subclass used to display a
+/// view that indicates the currently selected cell. You can subclass
+/// this type if you need further customization; just override the
+/// `indicatorClass` property in `PagingViewController`.
+@available(iOS 14.0, *)
+final class PagingHostingIndicatorView: PagingIndicatorView {
+ private let hostingController: UIHostingController
+
+ override init(frame: CGRect) {
+ let configuration = PagingIndicatorConfiguration(backgroundColor: .clear)
+ let rootView = PagingIndicator(configuration: configuration)
+ self.hostingController = UIHostingController(rootView: rootView)
+
+ super.init(frame: frame)
+
+ hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ hostingController.view.backgroundColor = .clear
+ hostingController.view.clipsToBounds = false
+ clipsToBounds = false
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func didMoveToWindow() {
+ super.didMoveToWindow()
+ if window == nil {
+ hostingController.willMove(toParent: nil)
+ hostingController.removeFromParent()
+ hostingController.didMove(toParent: nil)
+ } else if let parent = parentViewController() {
+ hostingController.willMove(toParent: parent)
+ parent.addChild(hostingController)
+ addSubview(hostingController.view)
+ hostingController.didMove(toParent: parent)
+ }
+ }
+
+ public override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ if let attributes = layoutAttributes as? PagingIndicatorLayoutAttributes {
+ let configuration = PagingIndicatorConfiguration(
+ backgroundColor: Color(attributes.backgroundColor ?? .clear)
+ )
+ hostingController.rootView = PagingIndicator(configuration: configuration)
+ hostingController.view.frame = bounds
+ }
+ }
+
+ private func parentViewController() -> UIViewController? {
+ var responder: UIResponder? = self
+ while let nextResponder = responder?.next {
+ if let viewController = nextResponder as? UIViewController {
+ return viewController
+ }
+ responder = nextResponder
+ }
+ return nil
+ }
+}
diff --git a/Parchment/Classes/PagingSizeCache.swift b/Parchment/Classes/PagingSizeCache.swift
index 56cd175c..b1dd78d4 100644
--- a/Parchment/Classes/PagingSizeCache.swift
+++ b/Parchment/Classes/PagingSizeCache.swift
@@ -12,18 +12,12 @@ class PagingSizeCache {
init(options: PagingOptions) {
self.options = options
- let didEnterBackground = UIApplication.didEnterBackgroundNotification
- let didReceiveMemoryWarning = UIApplication.didReceiveMemoryWarningNotification
-
- NotificationCenter.default.addObserver(self,
- selector: #selector(applicationDidEnterBackground(notification:)),
- name: didEnterBackground,
- object: nil)
-
- NotificationCenter.default.addObserver(self,
- selector: #selector(didReceiveMemoryWarning(notification:)),
- name: didReceiveMemoryWarning,
- object: nil)
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(didReceiveMemoryWarning(notification:)),
+ name: UIApplication.didReceiveMemoryWarningNotification,
+ object: nil
+ )
}
deinit {
@@ -58,8 +52,4 @@ class PagingSizeCache {
@objc private func didReceiveMemoryWarning(notification _: NSNotification) {
clear()
}
-
- @objc private func applicationDidEnterBackground(notification _: NSNotification) {
- clear()
- }
}
diff --git a/Parchment/Classes/PagingView.swift b/Parchment/Classes/PagingView.swift
index ef414ac6..1822ec0b 100644
--- a/Parchment/Classes/PagingView.swift
+++ b/Parchment/Classes/PagingView.swift
@@ -58,54 +58,31 @@ open class PagingView: UIView {
collectionView.translatesAutoresizingMaskIntoConstraints = false
pageView.translatesAutoresizingMaskIntoConstraints = false
- let metrics = [
- "height": options.menuHeight,
- ]
+ let heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: options.menuHeight)
+ heightConstraint.isActive = true
+ heightConstraint.priority = .defaultHigh
+ self.heightConstraint = heightConstraint
+
+ NSLayoutConstraint.activate([
+ collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
+ pageView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ pageView.trailingAnchor.constraint(equalTo: trailingAnchor)
+ ])
- let views = [
- "collectionView": collectionView,
- "pageView": pageView,
- ]
-
- let formatOptions = NSLayoutConstraint.FormatOptions()
-
- let horizontalCollectionViewContraints = NSLayoutConstraint.constraints(
- withVisualFormat: "H:|[collectionView]|",
- options: formatOptions,
- metrics: metrics,
- views: views
- )
-
- let horizontalPagingContentViewContraints = NSLayoutConstraint.constraints(
- withVisualFormat: "H:|[pageView]|",
- options: formatOptions,
- metrics: metrics,
- views: views
- )
-
- let verticalConstraintsFormat: String
switch options.menuPosition {
case .top:
- verticalConstraintsFormat = "V:|[collectionView(==height)][pageView]|"
+ NSLayoutConstraint.activate([
+ collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
+ pageView.topAnchor.constraint(equalTo: collectionView.bottomAnchor),
+ pageView.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
case .bottom:
- verticalConstraintsFormat = "V:|[pageView][collectionView(==height)]|"
- }
-
- let verticalContraints = NSLayoutConstraint.constraints(
- withVisualFormat: verticalConstraintsFormat,
- options: formatOptions,
- metrics: metrics,
- views: views
- )
-
- addConstraints(horizontalCollectionViewContraints)
- addConstraints(horizontalPagingContentViewContraints)
- addConstraints(verticalContraints)
-
- for constraint in verticalContraints {
- if constraint.firstAttribute == NSLayoutConstraint.Attribute.height {
- heightConstraint = constraint
- }
+ NSLayoutConstraint.activate([
+ pageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
+ pageView.bottomAnchor.constraint(equalTo: collectionView.topAnchor),
+ collectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
+ ])
}
}
}
diff --git a/Parchment/Classes/PagingViewController.swift b/Parchment/Classes/PagingViewController.swift
index b87dc490..785683f5 100644
--- a/Parchment/Classes/PagingViewController.swift
+++ b/Parchment/Classes/PagingViewController.swift
@@ -266,7 +266,7 @@ open class PagingViewController:
/// An instance that stores all the customization so that it's
/// easier to share between other classes.
- public private(set) var options: PagingOptions {
+ public internal(set) var options: PagingOptions {
didSet {
if options.menuLayoutClass != oldValue.menuLayoutClass {
let layout = createLayout(layout: options.menuLayoutClass.self)
@@ -287,6 +287,7 @@ open class PagingViewController:
private let pagingController: PagingController
private var didLayoutSubviews: Bool = false
+ private var didTransitionSize: Bool = false
private var pagingView: PagingView {
return view as! PagingView
@@ -488,6 +489,20 @@ open class PagingViewController:
configureContentInteraction()
}
+ open override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ if #unavailable(iOS 16), didTransitionSize {
+ view.layoutIfNeeded()
+ pagingController.transitionSize()
+ }
+ }
+
+ open override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ didTransitionSize = false
+ didLayoutSubviews = false
+ }
+
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
@@ -502,9 +517,10 @@ open class PagingViewController:
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
+ didTransitionSize = true
coordinator.animate(alongsideTransition: { _ in
self.pagingController.transitionSize()
- }, completion: nil)
+ })
}
/// Register cell class for paging cell
@@ -611,8 +627,8 @@ open class PagingViewController:
open func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let pagingItem = pagingController.visibleItems.pagingItem(for: indexPath)
- delegate?.pagingViewController(self, didSelectItem: pagingItem)
pagingController.select(indexPath: indexPath, animated: true)
+ delegate?.pagingViewController(self, didSelectItem: pagingItem)
}
open func collectionView(_: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
diff --git a/Parchment/Enums/PagingBorderOptions.swift b/Parchment/Enums/PagingBorderOptions.swift
index 6cfd043e..7d937096 100644
--- a/Parchment/Enums/PagingBorderOptions.swift
+++ b/Parchment/Enums/PagingBorderOptions.swift
@@ -4,7 +4,7 @@ public enum PagingBorderOptions {
case hidden
case visible(
height: CGFloat,
- zIndex: Int,
- insets: UIEdgeInsets
+ zIndex: Int = 0,
+ insets: UIEdgeInsets = .zero
)
}
diff --git a/Parchment/Enums/PagingIndicatorOptions.swift b/Parchment/Enums/PagingIndicatorOptions.swift
index df76c3d8..e7fe99e8 100644
--- a/Parchment/Enums/PagingIndicatorOptions.swift
+++ b/Parchment/Enums/PagingIndicatorOptions.swift
@@ -4,8 +4,8 @@ public enum PagingIndicatorOptions {
case hidden
case visible(
height: CGFloat,
- zIndex: Int,
- spacing: UIEdgeInsets,
- insets: UIEdgeInsets
+ zIndex: Int = 1,
+ spacing: UIEdgeInsets = .zero,
+ insets: UIEdgeInsets = .zero
)
}
diff --git a/Parchment/Enums/PagingMenuInteraction.swift b/Parchment/Enums/PagingMenuInteraction.swift
index 54e7191f..bccbeea4 100644
--- a/Parchment/Enums/PagingMenuInteraction.swift
+++ b/Parchment/Enums/PagingMenuInteraction.swift
@@ -3,5 +3,6 @@ import Foundation
public enum PagingMenuInteraction {
case scrolling
case swipe
+ case wheel
case none
}
diff --git a/Parchment/Protocols/CollectionView.swift b/Parchment/Protocols/CollectionView.swift
index f1c6958d..40502437 100644
--- a/Parchment/Protocols/CollectionView.swift
+++ b/Parchment/Protocols/CollectionView.swift
@@ -36,6 +36,7 @@ protocol CollectionView: AnyObject {
func layoutIfNeeded()
func setContentOffset(_ contentOffset: CGPoint, animated: Bool)
func selectItem(at indexPath: IndexPath?, animated: Bool, scrollPosition: UICollectionView.ScrollPosition)
+ func indexPathForItem(at point: CGPoint) -> IndexPath?
}
extension UICollectionView: CollectionView {}
diff --git a/Parchment/Protocols/PagingIndicatorStyle.swift b/Parchment/Protocols/PagingIndicatorStyle.swift
new file mode 100644
index 00000000..7167f434
--- /dev/null
+++ b/Parchment/Protocols/PagingIndicatorStyle.swift
@@ -0,0 +1,53 @@
+import Foundation
+import SwiftUI
+
+@available(iOS 14.0, *)
+public protocol PagingIndicatorStyle {
+ associatedtype Body: View
+ typealias Configuration = PagingIndicatorConfiguration
+ @ViewBuilder func makeBody(configuration: Configuration) -> Body
+}
+
+@available(iOS 14.0, *)
+struct PagingIndicator: View {
+ let configuration: PagingIndicatorConfiguration
+
+ @Environment(\.indicatorStyle) var style
+
+ var body: some View {
+ AnyView(style.makeBody(configuration: configuration))
+ }
+}
+
+@available(iOS 14.0, *)
+public struct PagingIndicatorConfiguration {
+ public let backgroundColor: Color
+}
+
+@available(iOS 14.0, *)
+struct DefaultPagingIndicatorStyle: PagingIndicatorStyle {
+ func makeBody(configuration: Configuration) -> some View {
+ Rectangle()
+ .fill(configuration.backgroundColor)
+ }
+}
+
+@available(iOS 14.0, *)
+struct PagingIndicatorStyleKey: EnvironmentKey {
+ static var defaultValue: any PagingIndicatorStyle = DefaultPagingIndicatorStyle()
+}
+
+@available(iOS 14.0, *)
+extension EnvironmentValues {
+ var indicatorStyle: any PagingIndicatorStyle {
+ get { self[PagingIndicatorStyleKey.self] }
+ set { self[PagingIndicatorStyleKey.self] = newValue }
+ }
+}
+
+@available(iOS 14.0, *)
+extension View {
+ public func indicatorStyle(_ style: some PagingIndicatorStyle) -> some View {
+ environment(\.indicatorStyle, style)
+ }
+}
diff --git a/Parchment/Protocols/PagingItem.swift b/Parchment/Protocols/PagingItem.swift
index 64ad937b..9551c3d9 100644
--- a/Parchment/Protocols/PagingItem.swift
+++ b/Parchment/Protocols/PagingItem.swift
@@ -29,3 +29,26 @@ extension PagingItem where Self: Hashable {
return hashValue
}
}
+
+/// The PagingIndexable protocol is used to compare items in your
+/// menu. Conform to this protocol when you need to mix multiple
+/// PagingItem types that all need to be compared.
+//
+/// The PagingIndexable protocol requires the conforming type to
+/// provide an index property of type Int, which is used to compare
+/// items in the menu.
+///
+/// For example, if you have a menu that contains both PagingIndexItem
+/// and PagingImageItem types, you can conform both types to
+/// PagingIndexable and Parchment will provide a default
+/// implementation that will be used to animate between them.
+public protocol PagingIndexable {
+ var index: Int { get }
+}
+
+extension PagingItem where Self: PagingIndexable {
+ public func isBefore(item: PagingItem) -> Bool {
+ guard let item = item as? PagingIndexable else { return false }
+ return index < item.index
+ }
+}
diff --git a/Parchment/Structs/Page.swift b/Parchment/Structs/Page.swift
new file mode 100644
index 00000000..d2221856
--- /dev/null
+++ b/Parchment/Structs/Page.swift
@@ -0,0 +1,274 @@
+import Foundation
+import UIKit
+import SwiftUI
+
+/// The `Page` struct represents a single page in a `PageView`.
+/// It contains the view hierarchy for the header and body of the
+/// page. You can initialize it with a custom SwiftUI header view
+/// using the `init(header:content:)` initializer, or just use
+/// the default title initializer `init(_:content:)`.
+///
+/// Usage:
+/// ```
+/// Page("Page Title") {
+/// Text("This is the content of the page.")
+/// }
+///
+/// Page { _ in
+/// Image(systemName: "star.fill")
+/// } content: {
+/// Text("This is the content of the page.")
+/// }
+/// ```
+///
+/// Note that the header and content parameters in both
+/// initializers are closures that return the view hierarchy for
+/// the header and body of the page, respectively.
+@available(iOS 14.0, *)
+public struct Page {
+ let reuseIdentifier: String
+ let pageIdentifier: String?
+ let header: (PagingOptions, PageState) -> UIContentConfiguration
+ let content: () -> UIViewController
+ let update: (UIViewController) -> Void
+
+ /// Creates a new page with the given header and content views.
+ ///
+ /// - Parameters:
+ /// - header: A closure that takes a `PageState` instance as
+ /// input and returns a `View` that represents the header view
+ /// for the page. The `PageState` instance will be updated as
+ /// the page is scrolled, allowing the header view to adjust
+ /// its appearance accordingly.
+ /// - content: A closure that returns a `View` that represents
+ /// the content view for the page.
+ ///
+ /// - Returns: A new `Page` instance with the given header and content views.
+ public init(
+ @ViewBuilder header: @escaping (PageState) -> Header,
+ @ViewBuilder content: () -> Content
+ ) {
+ let content = content()
+
+ self.reuseIdentifier = "CellIdentifier-\(String(describing: Header.self))"
+ self.pageIdentifier = nil
+
+ self.header = { options, state in
+ if #available(iOS 16.0, *) {
+ return UIHostingConfiguration {
+ PageCustomView(
+ content: header(state),
+ options: options,
+ state: state
+ )
+ }
+ .margins(.all, 0)
+ } else {
+ return PageContentConfiguration {
+ PageCustomView(
+ content: header(state),
+ options: options,
+ state: state
+ )
+ }
+ .margins(.all, 0)
+ }
+ }
+ self.content = {
+ UIHostingController(rootView: content)
+ }
+ self.update = { viewController in
+ let hostingController = viewController as! UIHostingController
+ hostingController.rootView = content
+ }
+ }
+
+ /// Creates a new page with the given header and content views.
+ ///
+ /// - Parameters:
+ /// - id: A unique identifier for this page.
+ /// - header: A closure that takes a `PageState` instance as
+ /// input and returns a `View` that represents the header view
+ /// for the page. The `PageState` instance will be updated as
+ /// the page is scrolled, allowing the header view to adjust
+ /// its appearance accordingly.
+ /// - content: A closure that returns a `View` that represents
+ /// the content view for the page.
+ ///
+ /// - Returns: A new `Page` instance with the given header and content views.
+ public init(
+ id: Id,
+ @ViewBuilder header: @escaping (PageState) -> Header,
+ @ViewBuilder content: () -> Content
+ ) {
+ let content = content()
+
+ self.reuseIdentifier = "CellIdentifier-\(String(describing: Header.self))"
+ self.pageIdentifier = id.description
+
+ self.header = { options, state in
+ if #available(iOS 16.0, *) {
+ return UIHostingConfiguration {
+ PageCustomView(
+ content: header(state),
+ options: options,
+ state: state
+ )
+ }
+ .margins(.all, 0)
+ } else {
+ return PageContentConfiguration {
+ PageCustomView(
+ content: header(state),
+ options: options,
+ state: state
+ )
+ }
+ .margins(.all, 0)
+ }
+ }
+ self.content = {
+ UIHostingController(rootView: content)
+ }
+ self.update = { viewController in
+ let hostingController = viewController as! UIHostingController
+ hostingController.rootView = content
+ }
+ }
+
+ /// Creates a new page with the given localized title and content views.
+ ///
+ /// - Parameters:
+ /// - titleKey: A `LocalizedStringKey` that represents the
+ /// localized title for the page. The title will be shown in a
+ /// `Text` view as the header of the page.
+ /// - content: A closure that returns a `View` that represents
+ /// the content view for the page.
+ ///
+ /// - Returns: A new `Page` instance with the given title and content views.
+ public init(
+ _ titleKey: LocalizedStringKey,
+ @ViewBuilder content: () -> Content
+ ) {
+ let content = content()
+
+ self.reuseIdentifier = "CellIdentifier-PageTitleView"
+ self.pageIdentifier = "PageIdentifier-\(titleKey)"
+
+ self.header = { options, state in
+ if #available(iOS 16.0, *) {
+ return UIHostingConfiguration {
+ PageTitleView(
+ content: Text(titleKey),
+ options: options,
+ progress: state.progress
+ )
+ }
+ .margins(.horizontal, options.menuItemLabelSpacing)
+ .margins(.vertical, 0)
+ } else {
+ return PageContentConfiguration {
+ PageTitleView(
+ content: Text(titleKey),
+ options: options,
+ progress: state.progress
+ )
+ }
+ .margins(.horizontal, options.menuItemLabelSpacing)
+ .margins(.vertical, 0)
+ }
+ }
+ self.content = {
+ UIHostingController(rootView: content)
+ }
+ self.update = { viewController in
+ let hostingController = viewController as! UIHostingController
+ hostingController.rootView = content
+ }
+ }
+
+ /// Creates a new page with the given title and content views.
+ ///
+ /// - Parameters:
+ /// - titleKey: A `StringProtocol` instance that represents
+ /// the title for the page. The title will be shown in a
+ /// `Text` view as the header of the page.
+ /// - content: A closure that returns a `View` that represents
+ /// the content view for the page.
+ ///
+ /// - Returns: A new `Page` instance with the given title and content views.
+ public init(
+ _ title: Title,
+ @ViewBuilder content: () -> Content
+ ) {
+ let content = content()
+
+ self.reuseIdentifier = "CellIdentifier-PageTitleView"
+ self.pageIdentifier = "PageIdentifier-\(title)"
+
+ self.header = { options, state in
+ if #available(iOS 16.0, *) {
+ return UIHostingConfiguration {
+ PageTitleView(
+ content: Text(title),
+ options: options,
+ progress: state.progress
+ )
+ }
+ .margins(.horizontal, options.menuItemLabelSpacing)
+ .margins(.vertical, 0)
+ } else {
+ return PageContentConfiguration {
+ PageTitleView(
+ content: Text(title),
+ options: options,
+ progress: state.progress
+ )
+ }
+ .margins(.horizontal, options.menuItemLabelSpacing)
+ .margins(.vertical, 0)
+ }
+ }
+ self.content = {
+ UIHostingController(rootView: content)
+ }
+ self.update = { viewController in
+ let hostingController = viewController as! UIHostingController
+ hostingController.rootView = content
+ }
+ }
+}
+
+@available(iOS 13.0, *)
+struct PageCustomView: View {
+ let content: Content
+ let options: PagingOptions
+ let state: PageState
+
+ var body: some View {
+ content
+ .fixedSize(horizontal: true, vertical: false)
+ .foregroundColor(Color(UIColor.interpolate(
+ from: options.textColor,
+ to: options.selectedTextColor,
+ with: state.progress
+ )))
+ }
+}
+
+@available(iOS 13.0, *)
+struct PageTitleView: View {
+ let content: Text
+ let options: PagingOptions
+ let progress: CGFloat
+
+ var body: some View {
+ content
+ .fixedSize(horizontal: true, vertical: false)
+ .foregroundColor(Color(UIColor.interpolate(
+ from: options.textColor,
+ to: options.selectedTextColor,
+ with: progress
+ )))
+ }
+}
diff --git a/Parchment/Structs/PageContentConfiguration.swift b/Parchment/Structs/PageContentConfiguration.swift
new file mode 100644
index 00000000..f0f1aa5f
--- /dev/null
+++ b/Parchment/Structs/PageContentConfiguration.swift
@@ -0,0 +1,108 @@
+import UIKit
+import SwiftUI
+
+@available(iOS 14.0, *)
+struct PageContentConfiguration: UIContentConfiguration {
+ let content: Content
+ var margins: NSDirectionalEdgeInsets
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ self.margins = .zero
+ }
+
+ func makeContentView() -> UIView & UIContentView {
+ return PageContentView(configuration: self)
+ }
+
+ func updated(for state: UIConfigurationState) -> PageContentConfiguration {
+ return self
+ }
+
+ func margins(_ edges: SwiftUI.Edge.Set = .all, _ length: CGFloat) -> PageContentConfiguration {
+ var configuration = self
+ configuration.margins = NSDirectionalEdgeInsets(
+ top: edges.contains(.top) ? length : margins.top,
+ leading: edges.contains(.leading) ? length : margins.leading,
+ bottom: edges.contains(.bottom) ? length : margins.bottom,
+ trailing: edges.contains(.trailing) ? length : margins.trailing
+ )
+ return configuration
+ }
+}
+
+@available(iOS 14.0, *)
+final class PageContentView: UIView, UIContentView {
+ var configuration: UIContentConfiguration {
+ didSet {
+ if let configuration = configuration as? PageContentConfiguration {
+ margins = configuration.margins
+ hostingController.rootView = configuration.content
+ directionalLayoutMargins = configuration.margins
+ }
+ }
+ }
+
+ override var intrinsicContentSize: CGSize {
+ return sizeThatFits(UIView.layoutFittingCompressedSize)
+ }
+
+ override func sizeThatFits(_ size: CGSize) -> CGSize {
+ let size = hostingController.sizeThatFits(in: size)
+ return CGSize(
+ width: size.width + margins.leading + margins.trailing,
+ height: size.height + margins.top + margins.bottom
+ )
+ }
+
+ private var margins: NSDirectionalEdgeInsets
+ private let hostingController: UIHostingController
+
+ init(configuration: PageContentConfiguration) {
+ self.configuration = configuration
+ self.hostingController = UIHostingController(rootView: configuration.content)
+ self.margins = configuration.margins
+ super.init(frame: .zero)
+ configure()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func didMoveToWindow() {
+ super.didMoveToWindow()
+ if window == nil {
+ hostingController.willMove(toParent: nil)
+ hostingController.removeFromParent()
+ hostingController.didMove(toParent: nil)
+ } else if let parent = parentViewController() {
+ hostingController.willMove(toParent: parent)
+ parent.addChild(hostingController)
+ hostingController.didMove(toParent: parent)
+ }
+ }
+
+ private func configure() {
+ hostingController.view.backgroundColor = .clear
+ addSubview(hostingController.view)
+ hostingController.view.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ hostingController.view.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
+ hostingController.view.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
+ hostingController.view.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
+ hostingController.view.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
+ ])
+ }
+
+ private func parentViewController() -> UIViewController? {
+ var responder: UIResponder? = self
+ while let nextResponder = responder?.next {
+ if let viewController = nextResponder as? UIViewController {
+ return viewController
+ }
+ responder = nextResponder
+ }
+ return nil
+ }
+}
diff --git a/Parchment/Structs/PageItem.swift b/Parchment/Structs/PageItem.swift
new file mode 100644
index 00000000..b2dce0ed
--- /dev/null
+++ b/Parchment/Structs/PageItem.swift
@@ -0,0 +1,21 @@
+import Foundation
+
+@available(iOS 14.0, *)
+struct PageItem: PagingItem, Hashable, Comparable {
+ let identifier: Int
+ let index: Int
+ let page: Page
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(identifier)
+ hasher.combine(index)
+ }
+
+ static func == (lhs: PageItem, rhs: PageItem) -> Bool {
+ return lhs.identifier == rhs.identifier && lhs.index == rhs.index
+ }
+
+ static func < (lhs: PageItem, rhs: PageItem) -> Bool {
+ return lhs.index < rhs.index
+ }
+}
diff --git a/Parchment/Structs/PageItemBuilder.swift b/Parchment/Structs/PageItemBuilder.swift
new file mode 100644
index 00000000..ccc9d080
--- /dev/null
+++ b/Parchment/Structs/PageItemBuilder.swift
@@ -0,0 +1,48 @@
+import SwiftUI
+
+@available(iOS 14.0, *)
+@resultBuilder
+public struct PageBuilder {
+ public static func buildExpression(_ expression: Page) -> [Page] {
+ return [expression]
+ }
+
+ public static func buildExpression(_ expression: Page?) -> [Page] {
+ if let expression = expression {
+ return [expression]
+ }
+ return []
+ }
+
+ public static func buildExpression(_ expression: [Page]) -> [Page] {
+ return expression
+ }
+
+ public static func buildBlock(_ components: [Page]) -> [Page] {
+ return components
+ }
+
+ public static func buildBlock(_ components: [Page]...) -> [Page] {
+ return components.reduce([], +)
+ }
+
+ public static func buildArray(_ components: [[Page]]) -> [Page] {
+ return components.flatMap { $0 }
+ }
+
+ public static func buildOptional(_ component: [Page]?) -> [Page] {
+ return component ?? []
+ }
+
+ public static func buildEither(first component: [Page]) -> [Page] {
+ return component
+ }
+
+ public static func buildEither(second component: [Page]) -> [Page] {
+ return component
+ }
+
+ public static func buildFor(_ component: [Page]) -> [Page] {
+ return component
+ }
+}
diff --git a/Parchment/Structs/PageItemCell.swift b/Parchment/Structs/PageItemCell.swift
new file mode 100644
index 00000000..d46d1ab5
--- /dev/null
+++ b/Parchment/Structs/PageItemCell.swift
@@ -0,0 +1,34 @@
+import UIKit
+import Foundation
+
+@available(iOS 14.0, *)
+final class PageItemCell: PagingCell {
+ private var page: Page!
+ private var options: PagingOptions?
+ private var itemSelected: Bool = false
+
+ override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options: PagingOptions) {
+ let item = pagingItem as! PageItem
+ let state = PageState(progress: selected ? 1 : 0, isSelected: selected)
+
+ self.page = item.page
+ self.options = options
+ self.itemSelected = selected
+
+ contentConfiguration = page.header(options, state)
+ backgroundColor = selected ? options.selectedBackgroundColor : options.backgroundColor
+ }
+
+ override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ if let attributes = layoutAttributes as? PagingCellLayoutAttributes, let options = options {
+ let state = PageState(progress: attributes.progress, isSelected: itemSelected)
+ contentConfiguration = page.header(options, state)
+ backgroundColor = UIColor.interpolate(
+ from: options.backgroundColor,
+ to: options.selectedBackgroundColor,
+ with: attributes.progress
+ )
+ }
+ }
+}
diff --git a/Parchment/Structs/PageState.swift b/Parchment/Structs/PageState.swift
new file mode 100644
index 00000000..414ec0ca
--- /dev/null
+++ b/Parchment/Structs/PageState.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+/// Represents the current state of a page. This will be passed into
+/// the `Page` struct while scrolling, and can be used to update the
+/// appearance of the corresponding menu item to reflect the current
+/// progress and selection state.
+public struct PageState {
+ public let progress: CGFloat
+ public let isSelected: Bool
+}
diff --git a/Parchment/Structs/PageView.swift b/Parchment/Structs/PageView.swift
index 26588b49..48052d3b 100644
--- a/Parchment/Structs/PageView.swift
+++ b/Parchment/Structs/PageView.swift
@@ -1,203 +1,457 @@
import SwiftUI
import UIKit
-/// Check if both SwiftUI and Combine is available. Without this
-/// xcodebuild fails, saying it can't find the SwiftUI types used
-/// inside PageView, even though it's wrapped with an @available
-/// check. Found a possible fix here: https://stackoverflow.com/questions/58233454/how-to-use-swiftui-in-framework
-/// This might be related to the issue discussed in this thread:
-/// https://forums.swift.org/t/weak-linking-of-frameworks-with-greater-deployment-targets/26017/24
-#if canImport(SwiftUI) && !(os(iOS) && (arch(i386) || arch(arm)))
-
- /// `PageView` provides a SwiftUI wrapper around `PagingViewController`.
- /// It can be used with any fixed array of `PagingItem`s. Use the
- /// `PagingOptions` struct to customize the properties.
- @available(iOS 13.0, *)
- public struct PageView: View {
- let content: (Item) -> Page
-
- private let options: PagingOptions
- private var items = [Item]()
- private var onWillScroll: ((PagingItem) -> Void)?
- private var onDidScroll: ((PagingItem) -> Void)?
- private var onDidSelect: ((PagingItem) -> Void)?
- @Binding private var selectedIndex: Int
-
- /// Initialize a new `PageView`.
- ///
- /// - Parameters:
- /// - options: The configuration parameters we want to customize.
- /// - items: The array of `PagingItem`s to display in the menu.
- /// - selectedIndex: The index of the currently selected page.
- /// Updating this index will transition to the new index.
- /// - content: A callback that returns the `View` for each item.
- public init(
- options: PagingOptions = PagingOptions(),
- items: [Item],
- selectedIndex: Binding = .constant(Int.max),
- content: @escaping (Item) -> Page
- ) {
- _selectedIndex = selectedIndex
- self.options = options
- self.items = items
- self.content = content
- }
+/// The `PageView` type is a SwiftUI view that allows the user to page
+/// between views while displaying a menu that moves with the
+/// content. It is a wrapper around the `PagingViewController` class
+/// in `Parchment`. To use the `PageView` type, create a new instance
+/// with a closure that returns an array of `Page` instances. Each
+/// `Page` instance contains a menu item view and a closure that
+/// returns the body of the page, which can be any SwiftUI view.
+///
+/// Usage:
+/// ```
+/// PageView {
+/// Page("Title 0") {
+/// Text("Page 0")
+/// }
+///
+/// Page("Title 1") {
+/// Text("Page 1")
+/// }
+/// }
+/// ```
+@available(iOS 14.0, *)
+public struct PageView: View {
+ private let items: [PagingItem]
+ private var content: ((PagingItem) -> UIViewController)?
+ private var options: PagingOptions
+ private var onWillScroll: ((PagingItem) -> Void)?
+ private var onDidScroll: ((PagingItem) -> Void)?
+ private var onDidSelect: ((PagingItem) -> Void)?
+
+ @Binding private var selectedIndex: Int
- public var body: some View {
- PagingController(
- items: items,
- options: options,
- content: content,
- onWillScroll: onWillScroll,
- onDidScroll: onDidScroll,
- onDidSelect: onDidSelect,
- selectedIndex: $selectedIndex
- )
+ static func defaultOptions() -> PagingOptions {
+ var options = PagingOptions()
+ options.menuItemSize = .selfSizing(estimatedWidth: 50, height: 50)
+ options.menuInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
+ options.indicatorOptions = .visible(height: 4, zIndex: .max, spacing: .zero, insets: .zero)
+ options.pagingContentBackgroundColor = .clear
+ options.menuBackgroundColor = .clear
+ options.backgroundColor = .clear
+ options.selectedBackgroundColor = .clear
+ options.selectedTextColor = .systemBlue
+ options.borderColor = .separator
+ options.indicatorColor = .systemBlue
+ options.textColor = .label
+ return options
+ }
+
+ /// Initializes a new `PageView` with the specified content.
+ ///
+ /// - Parameters:
+ /// - selectedIndex: A binding to an integer value that
+ /// represents the index of the currently selected
+ /// page. Defaults to a constant binding with a value of
+ /// `Int.max`, indicating no page is currently selected.
+ /// - content: A closure that returns an array of `Page`
+ /// instances. The `Page` type is a struct that represents the
+ /// content of a single page in the `PageView`. The closure
+ /// must return an array of `Page` instances, which will be
+ /// used to construct the `PageView`.
+ ///
+ /// - Returns: A new instance of `PageView`, initialized with the
+ /// selected index, and content.
+ public init(
+ selectedIndex: Binding = .constant(Int.max),
+ @PageBuilder content: () -> [Page]
+ ) {
+ _selectedIndex = selectedIndex
+ self.options = PageView.defaultOptions()
+ self.items = content()
+ .enumerated()
+ .map { (index, page) in
+ PageItem(
+ identifier: page.pageIdentifier?.hashValue ?? index,
+ index: index,
+ page: page
+ )
+ }
+ }
+
+ /// Initializes a new `PageView` based on the specified data.
+ ///
+ /// - Parameters:
+ /// - data: The identified data that the `PageView` instance
+ /// uses to create pages dynamically.
+ /// - selectedIndex: A binding to an integer value that
+ /// represents the index of the currently selected
+ /// page. Defaults to a constant binding with a value of
+ /// `Int.max`, indicating no page is currently selected.
+ /// - content: A page builder that creates pages
+ /// dynamically. The `Page` type is a struct that represents
+ /// the content of a single page in the `PageView`.
+ ///
+ /// - Returns: A new instance of `PageView`, initialized with the
+ /// selected index, and content.
+ public init(
+ _ data: Data,
+ selectedIndex: Binding = .constant(Int.max),
+ content: (Data.Element) -> Page
+ ) where Data.Element: Identifiable {
+ _selectedIndex = selectedIndex
+ self.options = PageView.defaultOptions()
+ self.items = data
+ .enumerated()
+ .map { (index, item) in
+ PageItem(
+ identifier: item.id.hashValue,
+ index: index,
+ page: content(item)
+ )
+ }
+ }
+
+ /// Initializes a new `PageView` based on the specified data.
+ ///
+ /// - Parameters:
+ /// - data: The identified data that the `PageView` instance
+ /// uses to create pages dynamically.
+ /// - id: The key path to the provided data's identifier.
+ /// - selectedIndex: A binding to an integer value that
+ /// represents the index of the currently selected
+ /// page. Defaults to a constant binding with a value of
+ /// `Int.max`, indicating no page is currently selected.
+ /// - content: A page builder that creates pages
+ /// dynamically. The `Page` type is a struct that represents
+ /// the content of a single page in the `PageView`.
+ ///
+ /// - Returns: A new instance of `PageView`, initialized with the
+ /// selected index, and content.
+ public init(
+ _ data: Data,
+ id: KeyPath,
+ selectedIndex: Binding = .constant(Int.max),
+ content: (Data.Element) -> Page
+ ) {
+ _selectedIndex = selectedIndex
+ self.options = PageView.defaultOptions()
+ self.items = data
+ .enumerated()
+ .map { (index, item) in
+ PageItem(
+ identifier: item[keyPath: id].hashValue,
+ index: index,
+ page: content(item)
+ )
+ }
+ }
+
+ /// Initialize a new `PageView`.
+ ///
+ /// - Parameters:
+ /// - options: The configuration parameters we want to customize.
+ /// - items: The array of `PagingItem`s to display in the menu.
+ /// - selectedIndex: The index of the currently selected page.
+ /// Updating this index will transition to the new index.
+ /// - content: A callback that returns the `View` for each item.
+ @available(*, deprecated, message: "This method is no longer recommended. Use the new Page initializers instead.")
+ public init(
+ options: PagingOptions = PagingOptions(),
+ items: [Item],
+ selectedIndex: Binding = .constant(Int.max),
+ content: @escaping (Item) -> Page
+ ) {
+ _selectedIndex = selectedIndex
+ self.options = options
+ self.items = items
+ self.content = { item in
+ let content = content(item as! Item)
+ return UIHostingController(rootView: content)
}
+ }
+
+ public var body: some View {
+ PagingControllerRepresentableView(
+ items: items,
+ content: content,
+ options: options,
+ onWillScroll: onWillScroll,
+ onDidScroll: onDidScroll,
+ onDidSelect: onDidSelect,
+ selectedIndex: $selectedIndex
+ )
+ }
+}
- /// Called when the user finished scrolling to a new view.
- ///
- /// - Parameter action: A closure that is called with the
- /// paging item that was scrolled to.
- /// - Returns: An instance of self
- public func didScroll(_ action: @escaping (PagingItem) -> Void) -> Self {
- var view = self
+@available(iOS 14.0, *)
+extension PageView {
+ /// Called when the user finished scrolling to a new view.
+ ///
+ /// - Parameter action: A closure that is called with the
+ /// paging item that was scrolled to.
+ /// - Returns: An instance of self
+ public func didScroll(_ action: @escaping (PagingItem) -> Void) -> Self {
+ var view = self
+ if let onDidScroll = view.onDidScroll {
+ view.onDidScroll = { item in
+ onDidScroll(item)
+ action(item)
+ }
+ } else {
view.onDidScroll = action
- return view
}
+ return view
+ }
- /// Called when the user is about to start scrolling to a new view.
- ///
- /// - Parameter action: A closure that is called with the
- /// paging item that is being scrolled to.
- /// - Returns: An instance of self
- public func willScroll(_ action: @escaping (PagingItem) -> Void) -> Self {
- var view = self
+ /// Called when the user is about to start scrolling to a new view.
+ ///
+ /// - Parameter action: A closure that is called with the
+ /// paging item that is being scrolled to.
+ /// - Returns: An instance of self
+ public func willScroll(_ action: @escaping (PagingItem) -> Void) -> Self {
+ var view = self
+ if let onWillScroll = view.onWillScroll {
+ view.onWillScroll = { item in
+ onWillScroll(item)
+ action(item)
+ }
+ } else {
view.onWillScroll = action
- return view
}
+ return view
+ }
- /// Called when an item was selected in the menu.
- ///
- /// - Parameter action: A closure that is called with the
- /// selected paging item.
- /// - Returns: An instance of self
- public func didSelect(_ action: @escaping (PagingItem) -> Void) -> Self {
- var view = self
+ /// Called when an item was selected in the menu.
+ ///
+ /// - Parameter action: A closure that is called with the
+ /// selected paging item.
+ /// - Returns: An instance of self
+ public func didSelect(_ action: @escaping (PagingItem) -> Void) -> Self {
+ var view = self
+ if let onDidSelect = view.onDidSelect {
+ view.onDidSelect = { item in
+ onDidSelect(item)
+ action(item)
+ }
+ } else {
view.onDidSelect = action
- return view
}
+ return view
+ }
+
+ /// The size for each of the menu items.
+ ///
+ /// Default:
+ /// ```
+ /// .selfSizing(estimatedWidth: 50, height: 50)
+ /// ```
+ public func menuItemSize(_ size: PagingMenuItemSize) -> Self {
+ var view = self
+ view.options.menuItemSize = size
+ return view
+ }
+
+ /// Determine the spacing between the menu items.
+ public func menuItemSpacing(_ spacing: CGFloat) -> Self {
+ var view = self
+ view.options.menuItemSpacing = spacing
+ return view
+ }
- /// Create a custom paging view controller subclass that we
- /// can use to store state to avoid reloading data unnecessary.
- final class CustomPagingViewController: PagingViewController {
- var items: [Item]?
+ /// Determine the horizontal spacing around the title label. This
+ /// only applies when using the default string initializer.
+ public func menuItemLabelSpacing(_ spacing: CGFloat) -> Self {
+ var view = self
+ view.options.menuItemLabelSpacing = spacing
+ return view
+ }
+
+ /// Determine the insets at around all the menu items,
+ public func menuInsets(_ insets: EdgeInsets) -> Self {
+ var view = self
+ view.options.menuInsets = UIEdgeInsets(
+ top: insets.top,
+ left: insets.leading,
+ bottom: insets.bottom,
+ right: insets.trailing
+ )
+ return view
+ }
+
+ /// Determine the insets at around all the menu items.
+ public func menuInsets(_ edges: SwiftUI.Edge.Set, _ length: CGFloat) -> Self {
+ var view = self
+ if edges.contains(.all) {
+ view.options.menuInsets.top = length
+ view.options.menuInsets.bottom = length
+ view.options.menuInsets.left = length
+ view.options.menuInsets.right = length
+ }
+ if edges.contains(.vertical) {
+ view.options.menuInsets.top = length
+ view.options.menuInsets.bottom = length
+ }
+ if edges.contains(.horizontal) {
+ view.options.menuInsets.left = length
+ view.options.menuInsets.right = length
+ }
+ if edges.contains(.top) {
+ view.options.menuInsets.top = length
+ }
+ if edges.contains(.bottom) {
+ view.options.menuInsets.bottom = length
}
+ if edges.contains(.leading) {
+ view.options.menuInsets.left = length
+ }
+ if edges.contains(.trailing) {
+ view.options.menuInsets.right = length
+ }
+ return view
+ }
- struct PagingController: UIViewControllerRepresentable {
- let items: [Item]
- let options: PagingOptions
- let content: (Item) -> Page
- var onWillScroll: ((PagingItem) -> Void)?
- var onDidScroll: ((PagingItem) -> Void)?
- var onDidSelect: ((PagingItem) -> Void)?
+ /// Determine the insets at around all the menu items.
+ public func menuInsets(_ length: CGFloat) -> Self {
+ var view = self
+ view.options.menuInsets = UIEdgeInsets(
+ top: length,
+ left: length,
+ bottom: length,
+ right: length
+ )
+ return view
+ }
- @Binding var selectedIndex: Int
+ /// Determine whether the menu items should be centered when all
+ /// the items can fit within the bounds of the view.
+ public func menuHorizontalAlignment(_ alignment: PagingMenuHorizontalAlignment) -> Self {
+ var view = self
+ view.options.menuHorizontalAlignment = alignment
+ return view
+ }
- func makeCoordinator() -> Coordinator {
- Coordinator(self)
- }
+ /// Determine the position of the menu relative to the content.
+ public func menuPosition(_ position: PagingMenuPosition) -> Self {
+ var view = self
+ view.options.menuPosition = position
+ return view
+ }
- func makeUIViewController(context: UIViewControllerRepresentableContext) -> CustomPagingViewController {
- let pagingViewController = CustomPagingViewController(options: options)
- pagingViewController.dataSource = context.coordinator
- pagingViewController.delegate = context.coordinator
- return pagingViewController
- }
+ /// Determine the transition behaviour of menu items while
+ /// scrolling the content.
+ public func menuTransition(_ transition: PagingMenuTransition) -> Self {
+ var view = self
+ view.options.menuTransition = transition
+ return view
+ }
- func updateUIViewController(_ pagingViewController: CustomPagingViewController,
- context: UIViewControllerRepresentableContext) {
- context.coordinator.parent = self
-
- if pagingViewController.dataSource == nil {
- pagingViewController.dataSource = context.coordinator
- }
-
- // If the menu items have changed we call reload data
- // to update both the menu and content views.
- if let previousItems = pagingViewController.items,
- !previousItems.elementsEqual(items, by: { $0.isEqual(to: $1) }) {
- pagingViewController.reloadData()
- }
-
- // Store the current items so we can compare it with
- // the new items the next time this method is called.
- pagingViewController.items = items
-
- // HACK: If the user don't pass a selectedIndex binding, the
- // default parameter is set to .constant(Int.max) which allows
- // us to check here if a binding was passed in or not (it
- // doesn't seem possible to make the binding itself optional).
- // This check is needed because we cannot update a .constant
- // value. When the user scroll to another page, the
- // selectedIndex binding will always be the same, so calling
- // `select(index:)` will select the wrong page. This fixes a bug
- // where the wrong page would be selected when rotating.
- guard selectedIndex != Int.max else {
- return
- }
-
- pagingViewController.select(index: selectedIndex, animated: true)
- }
- }
+ /// Determine how users can interact with the menu items.
+ public func menuInteraction(_ interaction: PagingMenuInteraction) -> Self {
+ var view = self
+ view.options.menuInteraction = interaction
+ return view
+ }
- final class Coordinator: PagingViewControllerDataSource, PagingViewControllerDelegate {
- var parent: PagingController
+ /// Determine how users can interact with the page view
+ /// controller.
+ public func contentInteraction(_ interaction: PagingContentInteraction) -> Self {
+ var view = self
+ view.options.contentInteraction = interaction
+ return view
+ }
- init(_ pagingController: PagingController) {
- parent = pagingController
- }
+ /// Determine how the selected menu item should be aligned when it
+ /// is selected. Effectively the same as the
+ /// `UICollectionViewScrollPosition`.
+ public func selectedScrollPosition(_ position: PagingSelectedScrollPosition) -> Self {
+ var view = self
+ view.options.selectedScrollPosition = position
+ return view
+ }
- func numberOfViewControllers(in _: PagingViewController) -> Int {
- return parent.items.count
- }
+ /// Add an indicator view to the selected menu item. The indicator
+ /// width will be equal to the selected menu items width. Insets
+ /// only apply horizontally.
+ public func indicatorOptions(_ options: PagingIndicatorOptions) -> Self {
+ var view = self
+ view.options.indicatorOptions = options
+ return view
+ }
- func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController {
- let view = parent.content(parent.items[index])
- let hostingViewController = UIHostingController(rootView: view)
- let backgroundColor = parent.options.pagingContentBackgroundColor
- hostingViewController.view.backgroundColor = backgroundColor
- return hostingViewController
- }
+ /// Add a border at the bottom of the menu items. The border will
+ /// be as wide as the menu items. Insets only apply horizontally.
+ public func borderOptions(_ options: PagingBorderOptions) -> Self {
+ var view = self
+ view.options.borderOptions = options
+ return view
+ }
- func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem {
- parent.items[index]
- }
+ /// The scroll navigation orientation of the content in the page
+ /// view controller.
+ public func contentNavigationOrientation(_ orientation: PagingNavigationOrientation) -> Self {
+ var view = self
+ view.options.contentNavigationOrientation = orientation
+ return view
+ }
+}
- func pagingViewController(_ controller: PagingViewController,
- didScrollToItem pagingItem: PagingItem,
- startingViewController _: UIViewController?,
- destinationViewController _: UIViewController,
- transitionSuccessful _: Bool) {
- if let item = pagingItem as? Item,
- let index = parent.items.firstIndex(where: { $0.isEqual(to: item) }) {
- parent.selectedIndex = index
- }
+@available(iOS 14.0, *)
+extension PageView {
+ /// Determine the color of the indicator view.
+ public func indicatorColor(_ color: Color) -> Self {
+ var view = self
+ view.options.indicatorColor = UIColor(color)
+ return view
+ }
- parent.onDidScroll?(pagingItem)
+ /// Determine the color of the border view.
+ public func borderColor(_ color: Color) -> Self {
+ var view = self
+ view.options.borderColor = UIColor(color)
+ return view
+ }
- }
+ /// The color of the menu items when selected.
+ public func selectedColor(_ color: Color) -> Self {
+ var view = self
+ view.options.selectedTextColor = UIColor(color)
+ return view
+ }
- func pagingViewController(_: PagingViewController,
- willScrollToItem pagingItem: PagingItem,
- startingViewController _: UIViewController,
- destinationViewController _: UIViewController) {
- parent.onWillScroll?(pagingItem)
- }
+ /// The foreground color of the menu items when not selected.
+ public func foregroundColor(_ color: Color) -> Self {
+ var view = self
+ view.options.textColor = UIColor(color)
+ return view
+ }
+
+ /// The background color for the menu items.
+ public func backgroundColor(_ color: Color) -> Self {
+ var view = self
+ view.options.backgroundColor = UIColor(color)
+ return view
+ }
- func pagingViewController(_: PagingViewController, didSelectItem pagingItem: PagingItem) {
- parent.onDidSelect?(pagingItem)
- }
- }
+ /// The background color for the selected menu item.
+ public func selectedBackgroundColor(_ color: Color) -> Self {
+ var view = self
+ view.options.selectedBackgroundColor = UIColor(color)
+ return view
+ }
+
+ /// The background color for the view behind the menu items.
+ public func menuBackgroundColor(_ color: Color) -> Self {
+ var view = self
+ view.options.menuBackgroundColor = UIColor(color)
+ return view
+ }
+
+ // The background color for the paging contents.
+ public func contentBackgroundColor(_ color: Color) -> Self {
+ var view = self
+ view.options.pagingContentBackgroundColor = UIColor(color)
+ return view
}
-#endif
+}
diff --git a/Parchment/Structs/PagingControllerRepresentableView.swift b/Parchment/Structs/PagingControllerRepresentableView.swift
new file mode 100644
index 00000000..66d18eeb
--- /dev/null
+++ b/Parchment/Structs/PagingControllerRepresentableView.swift
@@ -0,0 +1,94 @@
+import UIKit
+import SwiftUI
+
+@available(iOS 14.0, *)
+struct PagingControllerRepresentableView: UIViewControllerRepresentable {
+ let items: [PagingItem]
+ let content: ((PagingItem) -> UIViewController)?
+ let options: PagingOptions
+ var onWillScroll: ((PagingItem) -> Void)?
+ var onDidScroll: ((PagingItem) -> Void)?
+ var onDidSelect: ((PagingItem) -> Void)?
+
+ @Binding var selectedIndex: Int
+
+ func makeCoordinator() -> PageViewCoordinator {
+ PageViewCoordinator(self)
+ }
+
+ func makeUIViewController(
+ context: UIViewControllerRepresentableContext
+ ) -> PagingViewController {
+ let pagingViewController = PagingViewController(options: options)
+ pagingViewController.dataSource = context.coordinator
+ pagingViewController.delegate = context.coordinator
+ pagingViewController.indicatorClass = PagingHostingIndicatorView.self
+ pagingViewController.collectionView.clipsToBounds = false
+
+ if let items = items as? [PageItem] {
+ for item in items {
+ pagingViewController.collectionView.register(
+ PageItemCell.self,
+ forCellWithReuseIdentifier: item.page.reuseIdentifier
+ )
+ }
+ }
+
+ return pagingViewController
+ }
+
+ func updateUIViewController(
+ _ pagingViewController: PagingViewController,
+ context: UIViewControllerRepresentableContext
+ ) {
+ var oldItems: [Int: PagingItem] = [:]
+
+ for oldItem in context.coordinator.parent.items {
+ if let oldItem = oldItem as? PageItem {
+ oldItems[oldItem.identifier] = oldItem
+ }
+ }
+
+ context.coordinator.parent = self
+
+ if pagingViewController.dataSource == nil {
+ pagingViewController.dataSource = context.coordinator
+ }
+
+ pagingViewController.options = options
+ pagingViewController.indicatorClass = PagingHostingIndicatorView.self
+
+ // We only want to reload the content views when the items have actually
+ // changed. For items that are added, a new view controller instance will
+ // be created by the PageViewCoordinator.
+ if let currentItem = pagingViewController.state.currentPagingItem,
+ let pageItem = currentItem as? PageItem,
+ let oldItem = oldItems[pageItem.identifier] {
+ pagingViewController.reloadMenu()
+
+ if !oldItem.isEqual(to: currentItem) {
+ if let pageItem = currentItem as? PageItem,
+ let viewController = context.coordinator.controllers[currentItem.identifier]?.value {
+ pageItem.page.update(viewController)
+ }
+ }
+ } else {
+ pagingViewController.reloadData()
+ }
+
+ // HACK: If the user don't pass a selectedIndex binding, the
+ // default parameter is set to .constant(Int.max) which allows
+ // us to check here if a binding was passed in or not (it
+ // doesn't seem possible to make the binding itself optional).
+ // This check is needed because we cannot update a .constant
+ // value. When the user scroll to another page, the
+ // selectedIndex binding will always be the same, so calling
+ // `select(index:)` will select the wrong page. This fixes a bug
+ // where the wrong page would be selected when rotating.
+ guard selectedIndex != Int.max else {
+ return
+ }
+
+ pagingViewController.select(index: selectedIndex, animated: true)
+ }
+}
diff --git a/Parchment/Structs/PagingIndexItem.swift b/Parchment/Structs/PagingIndexItem.swift
index 12749b7d..cb0c7a33 100644
--- a/Parchment/Structs/PagingIndexItem.swift
+++ b/Parchment/Structs/PagingIndexItem.swift
@@ -3,7 +3,7 @@ import UIKit
/// An implementation of the `PagingItem` protocol that stores the
/// index and title of a given item. The index property is needed to
/// make the `PagingItem` comparable.
-public struct PagingIndexItem: PagingItem, Hashable, Comparable {
+public struct PagingIndexItem: PagingItem, PagingIndexable, Hashable {
/// The index of the `PagingItem` instance
public let index: Int
@@ -18,8 +18,4 @@ public struct PagingIndexItem: PagingItem, Hashable, Comparable {
self.index = index
self.title = title
}
-
- public static func < (lhs: PagingIndexItem, rhs: PagingIndexItem) -> Bool {
- return lhs.index < rhs.index
- }
}
diff --git a/ParchmentTests/Mocks/MockCollectionView.swift b/ParchmentTests/Mocks/MockCollectionView.swift
index 3fec885d..64724990 100644
--- a/ParchmentTests/Mocks/MockCollectionView.swift
+++ b/ParchmentTests/Mocks/MockCollectionView.swift
@@ -15,6 +15,7 @@ final class MockCollectionView: CollectionView, Mock {
animated: Bool,
scrollPosition: UICollectionView.ScrollPosition
)
+ case indexPathForItem(point: CGPoint)
}
var visibleItems: (() -> Int)!
@@ -113,6 +114,13 @@ final class MockCollectionView: CollectionView, Mock {
}
}
+ func indexPathForItem(at point: CGPoint) -> IndexPath? {
+ calls.append(MockCall(
+ action: .collectionView(.indexPathForItem(point: point))
+ ))
+ return nil
+ }
+
func register(_: AnyClass?, forCellWithReuseIdentifier _: String) {
return
}
diff --git a/ParchmentTests/PageViewManagerTests.swift b/ParchmentTests/PageViewManagerTests.swift
index 4af0d30c..ca537b64 100644
--- a/ParchmentTests/PageViewManagerTests.swift
+++ b/ParchmentTests/PageViewManagerTests.swift
@@ -737,12 +737,12 @@ final class PageViewManagerTests: XCTestCase {
manager.didScroll(progress: -0.01)
// Expect that it triggers .isScrolling events for scroll events
- // when overshooting, but does not trigger appereance transitions
+ // when overshooting, but does not trigger appearance transitions
// for the next upcoming view (viewController3).
XCTAssertEqual(delegate.calls, [
- .isScrolling(from: viewController1, to: viewController2, progress: 0.0),
- .isScrolling(from: viewController1, to: viewController2, progress: 0.01),
- .isScrolling(from: viewController1, to: viewController2, progress: -0.01),
+ .isScrolling(from: viewController2, to: viewController3, progress: 0.0),
+ .isScrolling(from: viewController2, to: viewController3, progress: 0.01),
+ .isScrolling(from: viewController2, to: viewController3, progress: -0.01),
])
}
diff --git a/ParchmentTests/PagingControllerTests.swift b/ParchmentTests/PagingControllerTests.swift
index b9e4face..c4317b92 100644
--- a/ParchmentTests/PagingControllerTests.swift
+++ b/ParchmentTests/PagingControllerTests.swift
@@ -1,6 +1,6 @@
import Foundation
-@testable import Parchment
import XCTest
+@testable import Parchment
final class PagingControllerTests: XCTestCase {
static let ItemSize: CGFloat = 50
@@ -13,6 +13,7 @@ final class PagingControllerTests: XCTestCase {
var sizeDelegate: MockPagingControllerSizeDelegate?
var pagingController: PagingController!
+ @MainActor
override func setUp() {
options = PagingOptions()
options.selectedScrollPosition = .left
@@ -49,6 +50,7 @@ final class PagingControllerTests: XCTestCase {
// MARK: - Content scrolled
+ @MainActor
func testContentScrolledFromSelectedProgressPositive() {
// Select the first item.
pagingController.select(pagingItem: Item(index: 3), animated: false)
@@ -80,10 +82,11 @@ final class PagingControllerTests: XCTestCase {
)),
.collectionViewLayout(.invalidateLayoutWithContext(
invalidateSizes: false
- )),
+ )),
])
}
+ @MainActor
func testContentScrolledFromSelectedProgressNegative() {
// Select the first item.
pagingController.select(pagingItem: Item(index: 3), animated: false)
@@ -115,10 +118,11 @@ final class PagingControllerTests: XCTestCase {
)),
.collectionViewLayout(.invalidateLayoutWithContext(
invalidateSizes: false
- )),
+ )),
])
}
+ @MainActor
func testContentOffsetFromSelectedProgressZero() {
// Select the first item.
pagingController.select(pagingItem: Item(index: 3), animated: false)
@@ -135,9 +139,10 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(collectionViewLayout.calls, [])
XCTAssertEqual(pagingController.state, PagingState.selected(
pagingItem: Item(index: 3)
- ))
+ ))
}
+ @MainActor
func testContentScrolledNoUpcomingPagingItem() {
// Prevent the data source from returning an upcoming item.
dataSource.maxIndexAfter = 3
@@ -157,10 +162,11 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(actions, [
.collectionViewLayout(.invalidateLayoutWithContext(
invalidateSizes: false
- )),
+ )),
])
}
+ @MainActor
func testContentScrolledSizeDelegate() {
// Setup the size delegate.
sizeDelegate = MockPagingControllerSizeDelegate()
@@ -177,9 +183,10 @@ final class PagingControllerTests: XCTestCase {
let action = collectionViewLayout.calls.last?.action
XCTAssertEqual(action, .collectionViewLayout(.invalidateLayoutWithContext(
invalidateSizes: true
- )))
+ )))
}
+ @MainActor
func testContentScrolledNoUpcomingPagingItemAndSizeDelegate() {
// Prevent the data source from returning an upcoming item.
dataSource.maxIndexAfter = 3
@@ -204,10 +211,11 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(actions, [
.collectionViewLayout(.invalidateLayoutWithContext(
invalidateSizes: false
- )),
+ )),
])
}
+ @MainActor
func testContentScrolledUpcomingItemOutsideVisibleItems() {
// Select the first item, and scroll to the edge of the
// collection view a few times to make sure the selected
@@ -259,10 +267,11 @@ final class PagingControllerTests: XCTestCase {
)),
.collectionViewLayout(.invalidateLayoutWithContext(
invalidateSizes: false
- )),
+ )),
])
}
+ @MainActor
func testContentScrolledProgressChangedFromPositiveToNegative() {
// Select an item and enter the scrolling state.
pagingController.select(pagingItem: Item(index: 1), animated: false)
@@ -280,9 +289,10 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(collectionViewLayout.calls, [])
XCTAssertEqual(pagingController.state, PagingState.selected(
pagingItem: Item(index: 1)
- ))
+ ))
}
+ @MainActor
func testContentScrolledProgressChangedFromNegativeToPositive() {
// Select an item and enter the scrolling state.
pagingController.select(pagingItem: Item(index: 1), animated: false)
@@ -300,9 +310,10 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(collectionViewLayout.calls, [])
XCTAssertEqual(pagingController.state, PagingState.selected(
pagingItem: Item(index: 1)
- ))
+ ))
}
+ @MainActor
func testContentScrolledProgressChangedToZero() {
// Select an item and enter the scrolling state.
pagingController.select(pagingItem: Item(index: 1), animated: false)
@@ -320,9 +331,10 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(collectionViewLayout.calls, [])
XCTAssertEqual(pagingController.state, PagingState.selected(
pagingItem: Item(index: 1)
- ))
+ ))
}
+ @MainActor
func testContentScrolledProgressChangedSameSign() {
// Select an item and enter the scrolling state.
pagingController.select(pagingItem: Item(index: 1), animated: false)
@@ -355,12 +367,13 @@ final class PagingControllerTests: XCTestCase {
)),
.collectionViewLayout(.invalidateLayoutWithContext(
invalidateSizes: false
- )),
+ )),
])
}
// MARK: - Select item
+ @MainActor
func testSelectWhileEmpty() {
// Make sure there is no item before index 0.
dataSource.minIndexBefore = 0
@@ -375,7 +388,7 @@ final class PagingControllerTests: XCTestCase {
// Expect it to enter selected state.
XCTAssertEqual(pagingController.state, PagingState.selected(
pagingItem: Item(index: 0)
- ))
+ ))
// Combine the method calls for the collection view,
// collection view layout and delegate to ensure that
@@ -405,6 +418,7 @@ final class PagingControllerTests: XCTestCase {
])
}
+ @MainActor
func testSelectWhileEmptyAndNoSuperview() {
// Remove the superview.
collectionView.superview = nil
@@ -418,9 +432,10 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(delegate.calls, [])
XCTAssertEqual(pagingController.state, PagingState.selected(
pagingItem: Item(index: 0)
- ))
+ ))
}
+ @MainActor
func testSelectWhileEmptyAndNoWindow() {
// Remove the window and make sure we have a superview.
collectionView.superview = UIView(frame: .zero)
@@ -434,9 +449,10 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(delegate.calls, [])
XCTAssertEqual(pagingController.state, PagingState.selected(
pagingItem: Item(index: 0)
- ))
+ ))
}
+ @MainActor
func testSelectItemWhileScrolling() {
// Select an item and enter the scrolling state.
pagingController.select(pagingItem: Item(index: 1), animated: false)
@@ -458,6 +474,7 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(pagingController.state, oldState)
}
+ @MainActor
func testSelectSameItem() {
// Select an item and enter the scrolling state.
pagingController.select(pagingItem: Item(index: 0), animated: false)
@@ -478,6 +495,7 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(pagingController.state, oldState)
}
+ @MainActor
func testSelectDifferentItem() {
// Make sure there is no item before index 0.
dataSource.minIndexBefore = 0
@@ -503,6 +521,7 @@ final class PagingControllerTests: XCTestCase {
))
}
+ @MainActor
func testSelectPreviousSibling() {
// Make sure there is no item before index 0.
dataSource.minIndexBefore = 0
@@ -530,6 +549,7 @@ final class PagingControllerTests: XCTestCase {
])
}
+ @MainActor
func testSelectNextSibling() {
// Make sure there is no item before index 0.
dataSource.minIndexBefore = 0
@@ -557,6 +577,7 @@ final class PagingControllerTests: XCTestCase {
])
}
+ @MainActor
func testSelectNotSibling() {
// Make sure there is no item before index 0.
dataSource.minIndexBefore = 0
@@ -584,6 +605,7 @@ final class PagingControllerTests: XCTestCase {
])
}
+ @MainActor
func testSelectItemOutsideVisibleItems() {
// Select the first item, and scroll to the edge of the
// collection view a few times to make sure the selected
@@ -646,6 +668,7 @@ final class PagingControllerTests: XCTestCase {
// MARK: - Content finished scrolling
+ @MainActor
func testContentFinishedScrollingWithUpcomingItem() {
// Select an item and enter the scrolling state.
dataSource.minIndexBefore = 0
@@ -688,6 +711,7 @@ final class PagingControllerTests: XCTestCase {
])
}
+ @MainActor
func testContentFinishedScrollingCollectionViewBeingDragged() {
// Select an item and enter the scrolling state.
dataSource.minIndexBefore = 0
@@ -709,6 +733,7 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(delegate.calls, [])
}
+ @MainActor
func testContentFinishedScrollingWihtoutUpcomingItem() {
// Select an item and enter the scrolling state.
dataSource.minIndexBefore = 0
@@ -729,6 +754,7 @@ final class PagingControllerTests: XCTestCase {
// MARK: - Transition size
+ @MainActor
func testTransitionSize() {
dataSource.minIndexBefore = 0
pagingController.select(pagingItem: Item(index: 0), animated: false)
@@ -766,6 +792,7 @@ final class PagingControllerTests: XCTestCase {
])
}
+ @MainActor
func testTransitionSizeWhenSelected() {
dataSource.minIndexBefore = 0
pagingController.select(pagingItem: Item(index: 0), animated: false)
@@ -782,6 +809,7 @@ final class PagingControllerTests: XCTestCase {
XCTAssertEqual(pagingController.state, .selected(pagingItem: Item(index: 0)))
}
+ @MainActor
func testTransitionSizeWhenScrolling() {
dataSource.minIndexBefore = 0
pagingController.select(pagingItem: Item(index: 0), animated: false)
@@ -803,6 +831,7 @@ final class PagingControllerTests: XCTestCase {
// MARK: - Reload data
+ @MainActor
func testReloadData() {
pagingController.reloadData(around: Item(index: 2))
@@ -846,6 +875,7 @@ final class PagingControllerTests: XCTestCase {
// MARK: - Reload menu
+ @MainActor
func testReloadMenu() {
pagingController.reloadMenu(around: Item(index: 2))
diff --git a/ParchmentTests/PagingViewControllerDelegateTests.swift b/ParchmentTests/PagingViewControllerDelegateTests.swift
new file mode 100644
index 00000000..877aa977
--- /dev/null
+++ b/ParchmentTests/PagingViewControllerDelegateTests.swift
@@ -0,0 +1,82 @@
+import Foundation
+@testable import Parchment
+import UIKit
+import XCTest
+
+final class PagingViewControllerDelegateTests: XCTestCase {
+ func testDidSelectItem() {
+ let viewController0 = UIViewController()
+ let viewController1 = UIViewController()
+ let pagingViewController = PagingViewController(viewControllers: [
+ viewController0,
+ viewController1
+ ])
+
+ let delegate = Delegate()
+ let window = UIWindow(frame: UIScreen.main.bounds)
+ window.rootViewController = pagingViewController
+ window.makeKeyAndVisible()
+ pagingViewController.view.layoutIfNeeded()
+ pagingViewController.delegate = delegate
+
+ let expectation = XCTestExpectation()
+
+ delegate.didSelectItem = { item in
+ let upcomingItem = pagingViewController.state.upcomingPagingItem as? PagingIndexItem
+ let item = item as! PagingIndexItem
+ XCTAssertEqual(item.index, 1)
+ XCTAssertEqual(upcomingItem, item)
+ expectation.fulfill()
+ }
+
+ let indexPath = IndexPath(item: 1, section: 0)
+ pagingViewController.collectionView.delegate?.collectionView?(
+ pagingViewController.collectionView,
+ didSelectItemAt: indexPath
+ )
+
+ wait(for: [expectation], timeout: 1)
+ }
+
+ func testDidScrollToItem() {
+ let viewController0 = UIViewController()
+ let viewController1 = UIViewController()
+ let pagingViewController = PagingViewController(viewControllers: [
+ viewController0,
+ viewController1
+ ])
+
+ let delegate = Delegate()
+ let window = UIWindow(frame: UIScreen.main.bounds)
+ window.rootViewController = pagingViewController
+ window.makeKeyAndVisible()
+ pagingViewController.view.layoutIfNeeded()
+ pagingViewController.delegate = delegate
+
+ let expectation = XCTestExpectation()
+
+ delegate.didSelectItem = { item in
+ let upcomingItem = pagingViewController.state.upcomingPagingItem as? PagingIndexItem
+ let item = item as! PagingIndexItem
+ XCTAssertEqual(item.index, 1)
+ XCTAssertEqual(upcomingItem, item)
+ expectation.fulfill()
+ }
+
+ let indexPath = IndexPath(item: 1, section: 0)
+ pagingViewController.collectionView.delegate?.collectionView?(
+ pagingViewController.collectionView,
+ didSelectItemAt: indexPath
+ )
+
+ wait(for: [expectation], timeout: 1)
+ }
+}
+
+private final class Delegate: PagingViewControllerDelegate {
+ var didSelectItem: ((PagingItem) -> Void)?
+
+ func pagingViewController(_ pagingViewController: PagingViewController, didSelectItem pagingItem: PagingItem) {
+ didSelectItem?(pagingItem)
+ }
+}
diff --git a/README.md b/README.md
index f70ed501..8230ca9e 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,6 @@
-
- ✨ New beta is out! Features a new and improved API for SwiftUI Try it now.
-
-
@@ -39,22 +35,127 @@ Parchment lets you page between view controllers while showing any type of gener
## Table of contents
- [Getting started](#getting-started)
- - [Basic usage](#basic-usage)
- - [Data source](#data-source)
- - [Infinite data source](#infinite-data-source)
- - [Selecting items](#selecting-items)
- - [Reloading data](#reloading-data)
- - [Delegate](#delegate)
- - [Size delegate](#size-delegate)
-- [Customization](#customization)
+ - [SwiftUI](#basic-usage)
+ - [Basic usage](#basic-usage)
+ - [Dynamic pages](#dynamic-pages)
+ - [Update selection](#update-selection)
+ - [Modifiers](#modifiers)
+ - [UIKit](#basic-usage-with-uikit)
+ - [Basic usage with UIKit](#basic-usage-with-uikit)
+ - [Data source](#data-source)
+ - [Infinite data source](#infinite-data-source)
+ - [Selecting items](#selecting-items)
+ - [Reloading data](#reloading-data)
+ - [Delegate](#delegate)
+ - [Size delegate](#size-delegate)
+ - [Customization](#customization)
+- [Options](#options)
- [Installation](#installation)
- [Changelog](#changelog)
- [Licence](#licence)
## Getting started
+Using UIKit? Go to [UIKit documentation](#basic-usage-with-uikit).
+
+
+
+SwiftUI
+
### Basic usage
+Create a `PageView` instance with the pages you want to show. Each `Page` takes a title and a content view, which can be any SwiftUI view.
+
+```swift
+PageView {
+ Page("Title 0") {
+ Text("Page 0")
+ }
+ Page("Title 1") {
+ Text("Page 1")
+ }
+}
+```
+
+By default, the menu items are displayed as titles, but you can also pass in any SwiftUI view as the menu item. The state parameter allows you to customize the menu item based on the selected state and scroll position of the view. For instance, you could show an icon that rotates based on its progress like this:
+
+```swift
+PageView {
+ Page { state in
+ Image(systemName: "star.fill")
+ .rotationEffect(Angle(degrees: 90 * state.progress))
+ } content: {
+ Text("Page 1")
+ }
+}
+```
+
+### Dynamic pages
+
+To create a `PageView` with a dynamic number of pages, you can pass in a collection of items where each item is mapped to a `Page`:
+
+```swift
+PageView(items, id: \.self) { item in
+ Page("Title \(item)") {
+ Text("Page \(item)")
+ }
+}
+```
+
+### Update selection
+
+To select specific items, you can pass a binding into `PageView` with the index of the currently selected item. When updating the binding, Parchment will scroll to the new index.
+
+```swift
+@State var selectedIndex: Int = 0
+...
+PageView(selectedIndex: $selectedIndex) {
+ Page("Title 1") {
+ Button("Next") {
+ selectedIndex = 1
+ }
+ }
+ Page("Title 2") {
+ Text("Page 2")
+ }
+}
+```
+
+### Modifiers
+
+You can customize the `PageView` using the following modifiers. See [Options](#options) for more details on each option.
+
+```swift
+PageView {
+ Page("Title 1") {
+ Text("Page 1")
+ }
+}
+.menuItemSize(.fixed(width: 100, height: 60))
+.menuItemSpacing(20)
+.menuItemLabelSpacing(30)
+.menuBackgroundColor(.white)
+.menuInsets(.vertical, 20)
+.menuHorizontalAlignment(.center)
+.menuPosition(.bottom)
+.menuTransition(.scrollAlongside)
+.menuInteraction(.swipe)
+.contentInteraction(.scrolling)
+.contentNavigationOrientation(.vertical)
+.selectedScrollPosition(.preferCentered)
+.indicatorOptions(.visible(height: 4))
+.indicatorColor(.blue)
+.borderOptions(.visible(height: 4))
+.borderColor(.blue.opacity(0.2))
+```
+
+
+
+
+UIKit
+
+### Basic usage with UIKit
+
Parchment is built around the `PagingViewController` class. You can initialize it with an array of view controllers and it will display menu items for each view controller using their `title` property.
```Swift
@@ -244,7 +345,7 @@ let pagingViewController = PagingViewController()
pagingViewController.sizeDelegate = self
```
-## Customization
+### Customization
Parchment is built to be very flexible. The menu items are displayed using UICollectionView, so they can display pretty much whatever you want. If you need any further customization you can even subclass the collection view layout. All customization is handled by the properties listed below.
@@ -269,6 +370,12 @@ pagingViewController.menuItemSize = .fixed(width: 40, height: 40)
pagingViewController.menuItemSpacing = 10
```
+See [Options](#options) for all customization options.
+
+
+
+## Options
+
#### `menuItemSize`
The size of the menu items. When using [`sizeDelegate`](#size-delegate) the width will be ignored.