Skip to content
Open
2 changes: 1 addition & 1 deletion EMPageViewController.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "EMPageViewController"
s.version = "4.0.0"
s.version = "4.0.1"
s.summary = "A better page view controller for iOS."
s.homepage = "https://github.com/emalyak/EMPageViewController"
s.license = { :type => "MIT", :file => "LICENSE" }
Expand Down
103 changes: 94 additions & 9 deletions EMPageViewController/EMPageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ import UIKit
- parameter transitionSuccessful: A Boolean whether the transition to the destination view controller was successful or not. If `true`, the new selected view controller is `destinationViewController`. If `false`, the transition returned to the view controller it started from, so the selected view controller is still `startingViewController`.
*/
@objc optional func em_pageViewController(_ pageViewController: EMPageViewController, didFinishScrollingFrom startingViewController: UIViewController?, destinationViewController:UIViewController, transitionSuccessful: Bool)

@objc optional func em_pageViewController(_ pageViewController: EMPageViewController, scrollViewDidEndDragging: UIScrollView, willDecelerate decelerate: Bool)

@objc optional func em_pageViewController(_ pageViewController: EMPageViewController, scrollViewDidEndScrolling: UIScrollView)
}

/**
Expand All @@ -120,7 +124,7 @@ import UIKit
}

/// Manages page navigation between view controllers. View controllers can be navigated via swiping gestures, or called programmatically.
open class EMPageViewController: UIViewController, UIScrollViewDelegate {
open class EMPageViewController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {

/// The object that provides view controllers on an as-needed basis throughout the navigation of the page view controller.
///
Expand All @@ -139,9 +143,13 @@ open class EMPageViewController: UIViewController, UIScrollViewDelegate {
return self.navigationOrientation == .horizontal
}

private var isChangeSize = false

@objc open var shouldRecognizeSimultaneously = false

/// The underlying `UIScrollView` responsible for scrolling page views.
/// - important: Properties should be set with caution to prevent unexpected behavior.
open private(set) lazy var scrollView: UIScrollView = {
@objc open private(set) lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.scrollsToTop = false
Expand All @@ -152,17 +160,20 @@ open class EMPageViewController: UIViewController, UIScrollViewDelegate {
scrollView.translatesAutoresizingMaskIntoConstraints = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
}
return scrollView
}()

/// The view controller before the selected view controller.
private var beforeViewController: UIViewController?
@objc open private(set) var beforeViewController: UIViewController?

/// The currently selected view controller. Can be `nil` if no view controller is selected.
@objc open private(set) var selectedViewController: UIViewController?

/// The view controller after the selected view controller.
private var afterViewController: UIViewController?
@objc open private(set) var afterViewController: UIViewController?

/// Boolean that indicates whether the page controller is currently in the process of scrolling.
@objc open private(set) var scrolling = false
Expand All @@ -180,7 +191,7 @@ open class EMPageViewController: UIViewController, UIScrollViewDelegate {
/// Initializes a newly created page view controller with the specified navigation orientation.
/// - parameter navigationOrientation: The page view controller's navigation scroll direction.
/// - returns: The initialized page view controller.
public convenience init(navigationOrientation: EMPageViewControllerNavigationOrientation) {
@objc public convenience init(navigationOrientation: EMPageViewControllerNavigationOrientation) {
self.init()
self.navigationOrientation = navigationOrientation
}
Expand All @@ -199,19 +210,61 @@ open class EMPageViewController: UIViewController, UIScrollViewDelegate {
@objc open func selectViewController(_ viewController: UIViewController, direction: EMPageViewControllerNavigationDirection, animated: Bool, completion: ((_ transitionSuccessful: Bool) -> Void)?) {

if (direction == .forward) {
self.afterViewController = viewController
if afterViewController != viewController {
removeChildIfNeeded(afterViewController)
afterViewController = viewController
}
self.layoutViews()
self.loadNewAdjoiningViewControllersOnFinish = true
self.scrollForward(animated: animated, completion: completion)
} else if (direction == .reverse) {
self.beforeViewController = viewController
if beforeViewController != viewController {
removeChildIfNeeded(beforeViewController)
beforeViewController = viewController
}
self.layoutViews()
self.loadNewAdjoiningViewControllersOnFinish = true
self.scrollReverse(animated: animated, completion: completion)
}

}

/**
remove all controllers, so that can be reloaded
*/
@objc open func removeViewControllers() {
guard !scrolling else { return }
removeChildIfNeeded(beforeViewController)
removeChildIfNeeded(afterViewController)
removeChildIfNeeded(selectedViewController)
beforeViewController = nil
afterViewController = nil
selectedViewController = nil
}

/**
allow load after ViewController when new data added to dataSource, call this method when scrolling is false
*/
@objc open func loadAfterViewControllerIfPossible() {
guard !scrolling, nil == afterViewController,
let current = selectedViewController else { return }
loadAfterViewController(for: current)
if nil != afterViewController {
layoutViews()
}
}

/**
allow load before ViewController when new data added to dataSource, call this method when scrolling is false
*/
@objc open func loadBeforeViewControllerIfPossible() {
guard !scrolling, nil == beforeViewController,
let current = selectedViewController else { return }
loadBeforeViewController(for: current)
if nil != beforeViewController {
layoutViews()
}
}

/**
Transitions to the view controller right of the currently selected view controller in a horizontal orientation, or below the currently selected view controller in a vertical orientation. Also described as going to the next page.
Expand Down Expand Up @@ -295,7 +348,7 @@ open class EMPageViewController: UIViewController, UIScrollViewDelegate {
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()

guard !scrolling else {
guard !scrolling || isChangeSize else {
return
}

Expand Down Expand Up @@ -519,11 +572,16 @@ open class EMPageViewController: UIViewController, UIScrollViewDelegate {
self.layoutViews()
}

// MARK: - UIGestureRecognizerDelegate

func gestureRecognizer(_ gesture: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGesture: UIGestureRecognizer) -> Bool {
return shouldRecognizeSimultaneously
}

// MARK: - UIScrollView Delegate

open func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !adjustingContentOffset {
if !adjustingContentOffset && !isChangeSize {

let distance = self.isOrientationHorizontal ? self.view.bounds.width : self.view.bounds.height
let progress = ((self.isOrientationHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y) - distance) / distance
Expand Down Expand Up @@ -603,14 +661,41 @@ open class EMPageViewController: UIViewController, UIScrollViewDelegate {
(self.afterViewController != nil && self.beforeViewController == nil && scrollView.contentOffset.x > abs(scrollView.contentInset.left)) || // If it's at the beginning of the collection, the decelleration can't be triggered by scrolling away from, than torwards the inset
(self.beforeViewController != nil && self.afterViewController == nil && scrollView.contentOffset.x < abs(scrollView.contentInset.right)) { // Same as the last condition, but at the end of the collection
scrollView.setContentOffset(CGPoint(x: self.view.bounds.width, y: 0), animated: true)
} else {
delegate?.em_pageViewController?(self, scrollViewDidEndScrolling: scrollView)
}
} else {
if (self.beforeViewController != nil && self.afterViewController != nil) || // It isn't at the beginning or end of the page collection
(self.afterViewController != nil && self.beforeViewController == nil && scrollView.contentOffset.y > abs(scrollView.contentInset.top)) || // If it's at the beginning of the collection, the decelleration can't be triggered by scrolling away from, than torwards the inset
(self.beforeViewController != nil && self.afterViewController == nil && scrollView.contentOffset.y < abs(scrollView.contentInset.bottom)) { // Same as the last condition, but at the end of the collection
scrollView.setContentOffset(CGPoint(x: 0, y: self.view.bounds.height), animated: true)
} else {
delegate?.em_pageViewController?(self, scrollViewDidEndScrolling: scrollView)
}
}
}

public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
delegate?.em_pageViewController?(self, scrollViewDidEndDragging: scrollView, willDecelerate: decelerate)
if !decelerate {
delegate?.em_pageViewController?(self, scrollViewDidEndScrolling: scrollView)
}
}

public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
delegate?.em_pageViewController?(self, scrollViewDidEndScrolling: scrollView)
}

// MARK: - size change
open override func viewWillTransition(to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator) {

self.isChangeSize = true

super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { [weak self] _ in
self?.view.setNeedsLayout()
} completion: { [weak self] _ in
self?.isChangeSize = false
}
}
}