2025-04-09 18:24:58 +08:00

553 lines
22 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// JYPageController.swift
// JYPageController
//
// Created by wang tao on 2022/7/14.
//
import UIKit
@objc public protocol JYPageControllerDataSource {
///segmentview frame
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect
///frame
func pageController(_ pageController: JYPageController, frameForContainerView container: UIScrollView) -> CGRect
///indexitemtitle
func pageController(_ pageController: JYPageController, titleAt index: Int) -> String
///indexitem
@objc optional func pageController(_ pageController: JYPageController, customViewAt index: Int) -> UIView?
///indexitembadgeView(eg. /frame.size)
@objc optional func pageController(_ pageController: JYPageController, badgeViewAt index: Int) -> UIView?
///
func numberOfChildControllers() -> Int
///indexUIViewController
func childController(atIndex index: Int) -> JYPageChildContollerProtocol
}
@objc public protocol JYPageControllerDelegate {
///childController
@objc optional func pageController(_ pageController: JYPageController, didLoadChildController: UIViewController, index: Int)
///scrollViewchildController
@objc optional func pageController(_ pageController: JYPageController, didEnterControllerAt index: Int)
@objc optional func pageController(_ pageController: JYPageController, mainDidScroll offsetY: CGFloat)
}
open class JYPageController: UIViewController {
///config
public var config: JYPageConfig = JYPageConfig.init()
///headerView
public var headerView: UIView? {
didSet {
if let header = headerView {
headerHeight = header.frame.size.height - self.config.hoverOffset
mainScrollView.tableHeaderView = header
}
}
}
///scrollView
public var scrollView: UIScrollView? {
get {
if headerView != nil {
return mainScrollView
}else {
return nil
}
}
}
///header view height
private var headerHeight: CGFloat = 0
///index
public var selectedIndex: Int = 0
///delegate
weak public var delegate: JYPageControllerDelegate?
///dataSource
weak public var dataSource: JYPageControllerDataSource?
///childViewController cache
private var childControllerCache: NSCache = NSCache<NSString, UIViewController>()
///scrollViewvcvc
private var displayControllerCache = Dictionary<NSString, UIViewController>()
///childController scrollView cache
private var childScrollViewCache: Dictionary = Dictionary<NSString, UIScrollView?>()
///menuview frame
private var menuViewFrame: CGRect = .zero
///container(scrollView)frame
private var childControllerViewFrame: CGRect = .zero
///scorllView
private var scrollByDragging = false
///
private var currentOffsetX: CGFloat = 0
///headerViewmenuView
private var scrollToTop: Bool = false
///scrollViewcontentOffsetY
private var verScrollViewContentOffsetY: CGFloat = 0
deinit {
childScrollViewCache.forEach { (key: NSString, value: UIScrollView?) in
value?.removeObserver(self, forKeyPath: "contentOffset")
}
}
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
delegate = self
dataSource = self
/**
initpageConfig,menuConfig
eg.
config.showIndicatorLineView = false
config.selectedTitleColor = .red
config.normalTitleColor = .red
config.selectedTitleFont = .systemFont(ofSize: 18, weight: .medium)
config.normalTitleFont = .systemFont(ofSize: 18, weight: .medium)
config.menuItemMargin = 10
*/
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func viewDidLoad() {
super.viewDidLoad()
pageViewSetup()
segmentedView.select(selectedIndex)
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
mainScrollView.isScrollEnabled = headerView != nil
}
//MARK: - Public
///
public func reload() {
for i in 0 ..< childControllersCount {
let cacheKey = String(i) as NSString
if let childController = childControllerCache.object(forKey: cacheKey) {
childController.view.removeFromSuperview()
childController.willMove(toParent: nil)
childController.removeFromParent()
}
}
mainScrollView.removeFromSuperview()
childControllersCount = dataSource?.numberOfChildControllers() ?? 0
childControllerCache.removeAllObjects()
displayControllerCache.removeAll()
selectedIndex = 0
segmentedView.reload()
segmentedView.select(selectedIndex)
pageViewSetup()
pageContentScrollView.setContentOffset(CGPoint(x: CGFloat(selectedIndex) * childControllerViewFrame.width, y: 0), animated: false)
}
///menuviewscrollviewcontentsize
public func contentSizeForMenuView() -> CGSize {
return segmentedView.contentSize()
}
///menuviewframe
public func updateMenuViewFrame(frame: CGRect) {
menuViewFrame = frame
segmentedView.updateFrame(frame: frame)
}
///indexmenuItembadgeView
public func insertMenuItemBadgeView(_ badgeView: UIView, atIndex index: Int) {
segmentedView.addSegmentedItemBadgeView(badgeView, atIndex: index)
}
///indexmenuItembadgeView
public func removeMenuItemBadgeView(atIndex index: Int) {
segmentedView.removeSegmentedItemBadgeView(atIndex: index)
}
//MARK: - Private
private func pageViewSetup() {
guard let source = dataSource else {
return
}
pageContentScrollView.bounces = config.pageScrollBounces
menuViewFrame = source.pageController(self, frameForSegmentedView: segmentedView)
childControllerViewFrame = source.pageController(self, frameForContainerView: pageContentScrollView)
let mainScrollViewY : CGFloat = 0
// if let navBar = navigationController?.navigationBar {
// mainScrollViewY = navBar.frame.height + UIApplication.shared.statusBarFrame.size.height
// }
segmentedView.frame = menuViewFrame
pageContentScrollView.frame = childControllerViewFrame
mainScrollView.frame = CGRect(x: childControllerViewFrame.origin.x, y: mainScrollViewY, width: childControllerViewFrame.width, height: childControllerViewFrame.origin.y + childControllerViewFrame.height + self.config.hoverOffset)
// mainScrollView.frame = mainControllerViewFrame
let contentSize = CGSize(width: CGFloat(childControllersCount)*childControllerViewFrame.width, height: childControllerViewFrame.height)
pageContentScrollView.contentSize = contentSize
if config.segmentedViewShowInNavigationBar {
view.addSubview(pageContentScrollView)
navigationItem.titleView = segmentedView
}else {
view.addSubview(mainScrollView)
}
}
///indexcontroller
private func addChildController(index: Int) {
var childController = UIViewController()
let cacheKey = String(index) as NSString
if let controlller = childControllerCache.object(forKey: cacheKey) {
childController = controlller
}else {
if let controlller = dataSource?.childController(atIndex: index) {
if let childScrollView = controlller.fetchChildControllerScrollView?(),childScrollView.isKind(of: UIScrollView.classForCoder()) {
childScrollView.addObserver(self, forKeyPath: "contentOffset", options: [.old,.new], context: nil)
childScrollViewCache[cacheKey] = childScrollView
}
childControllerCache.setObject(controlller, forKey: cacheKey)
delegate?.pageController?(self, didLoadChildController: controlller, index: index)
childController = controlller
}
}
if displayControllerCache[cacheKey] == nil {
addChild(childController)
}
childController.view.frame = CGRect(x: CGFloat(index)*childControllerViewFrame.size.width, y: 0, width: childControllerViewFrame.size.width, height: childControllerViewFrame.size.height)
childController.didMove(toParent: self)
pageContentScrollView.addSubview(childController.view)
displayControllerCache[cacheKey] = childController
}
private func loadChildControllerIfNeeded() {
let offsetX = pageContentScrollView.contentOffset.x
if offsetX < 0 || offsetX > pageContentScrollView.contentSize.width {
return
}
guard offsetX.truncatingRemainder(dividingBy: childControllerViewFrame.size.width) == 0 else {
var targetIndex = 0
if offsetX > currentOffsetX {
targetIndex = Int(offsetX/childControllerViewFrame.size.width) + 1
}else {
targetIndex = Int(offsetX/childControllerViewFrame.size.width)
}
let cacheKey = String(targetIndex) as NSString
let controller = displayControllerCache[cacheKey]
if targetIndex < childControllersCount, controller == nil {
addChildController(index: targetIndex)
}
return
}
}
private func removeChildControllerIfNeeded() {
for i in 0 ..< childControllersCount {
let cacheKey = String(i) as NSString
if let childController = displayControllerCache[cacheKey], childControllerIsInScreen(childController) == false {
childController.view.removeFromSuperview()
childController.willMove(toParent: nil)
childController.removeFromParent()
displayControllerCache.removeValue(forKey: cacheKey)
}
}
}
private func childControllerIsInScreen(_ childController: UIViewController) -> Bool {
let offsetX = pageContentScrollView.contentOffset.x
let screenWidth = pageContentScrollView.frame.width
let childViewMaxX = childController.view.frame.maxX
let childViewMinX = childController.view.frame.minX
if childViewMaxX > offsetX, childViewMinX - offsetX < screenWidth {
return true
}else{
return false
}
}
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentOffset", headerHeight > 0 {
let cacheKey = String(selectedIndex) as NSString
//1.mainScrollViewcurrentChildListScrollViewsegmentedViewcurrentChildListScrollView
if let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint, newContentOffset.y > 0, mainScrollView.contentOffset.y < headerHeight {
let currentChildListScrollView = childScrollViewCache[cacheKey]
currentChildListScrollView??.contentOffset = .zero
}
//2.mainScrollViewscrollview
if config.headerRefreshLocation == .headerViewTop, let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint, newContentOffset.y < 0 {
let currentChildListScrollView = childScrollViewCache[cacheKey]
currentChildListScrollView??.contentOffset = .zero
}
//3.scrollView
if config.headerRefreshLocation == .childControllerViewTop {
//3.1segmentedViewheaderViewscrollview
if mainScrollView.contentOffset.y > 0, mainScrollView.contentOffset.y < headerHeight, let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint, newContentOffset.y < 0 {
let currentChildListScrollView = childScrollViewCache[cacheKey]
currentChildListScrollView??.contentOffset = .zero
}
}
}
}
//MARK: - Lazy
private lazy var childControllersCount: Int = {
return dataSource?.numberOfChildControllers() ?? 0
}()
private(set) lazy var segmentedView: JYSegmentedView = {
let segment = JYSegmentedView.init(pageConfig: config)
segment.dataSource = self
segment.delegate = self
return segment
}()
private lazy var pageContentScrollView : UIScrollView = {
let scrollView = UIScrollView()
// scrollView.backgroundColor = .white
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
scrollView.isPagingEnabled = true
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
}
return scrollView
}()
private lazy var mainScrollView : JYScrollView = {
let scrollView = JYScrollView(frame: .zero, style: .plain)
scrollView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
scrollView.backgroundColor = .clear
scrollView.showsVerticalScrollIndicator = false
scrollView.isScrollEnabled = false
scrollView.separatorStyle = .none
scrollView.delegate = self
scrollView.dataSource = self
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 15.0, *) {
scrollView.sectionHeaderTopPadding = 0
}
return scrollView
}()
}
//MARK: - UIScrollViewDelegate
extension JYPageController:UIScrollViewDelegate {
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView == pageContentScrollView {
currentOffsetX = scrollView.contentOffset.x
scrollByDragging = true
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == pageContentScrollView {
selectedIndex = Int(scrollView.contentOffset.x/scrollView.frame.width)
segmentedView.segmentedViewScrollEnd(byScrollEndDecelerating: scrollView)
delegate?.pageController?(self, didEnterControllerAt: selectedIndex)
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == pageContentScrollView {
removeChildControllerIfNeeded()
if scrollByDragging {
loadChildControllerIfNeeded()
segmentedView.segmentedViewScroll(by:scrollView)
currentOffsetX = scrollView.contentOffset.x
}
}
if scrollView == mainScrollView, headerView != nil,headerView?.frame.height ?? 0 > 0 {
let cacheKey = String(selectedIndex) as NSString
let currentChildListScrollView = childScrollViewCache[cacheKey]
//1.segmentedViewmainScrollView
if currentChildListScrollView??.contentOffset.y ?? 0 > 0 || scrollView.contentOffset.y >= headerHeight {
mainScrollView.contentOffset = CGPoint(x: 0, y: headerHeight)
}
//2.mainScrollView
if config.headerRefreshLocation == .childControllerViewTop, mainScrollView.contentOffset.y < 0 {
mainScrollView.contentOffset = .zero
}
//3.mainScrollView
if pageContentScrollView.contentOffset.x.truncatingRemainder(dividingBy: childControllerViewFrame.size.width) > 0, mainScrollView.contentOffset.y != verScrollViewContentOffsetY {
mainScrollView.setContentOffset(CGPoint(x: 0, y: verScrollViewContentOffsetY), animated: false)
}
verScrollViewContentOffsetY = mainScrollView.contentOffset.y
self.delegate?.pageController?(self, mainDidScroll: verScrollViewContentOffsetY)
}
}
}
//MARK: - JYPageControllerDelegate, JYPageControllerDataSource
extension JYPageController: JYPageControllerDelegate, JYPageControllerDataSource {
open func pageController(_ pageView: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {
return .zero
}
open func pageController(_ pageView: JYPageController, frameForContainerView container: UIScrollView) -> CGRect {
return .zero
}
open func pageController(_ pageView: JYPageController, titleAt index: Int) -> String {
return ""
}
open func pageController(_ pageController: JYPageController, customViewAt index: Int) -> UIView? {
return nil
}
open func pageController(_ pageView: JYPageController, badgeViewAt index: Int) -> UIView? {
return nil
}
open func numberOfChildControllers() -> Int {
return 0
}
open func childController(atIndex index: Int) -> JYPageChildContollerProtocol {
return JYPlaceHolderController()
}
open func pageController(_ pageController: JYPageController, didLoadChildController: UIViewController, index: Int) {
}
open func pageController(_ pageController: JYPageController, didEnterControllerAt index: Int) {
}
}
//MARK: - JYSegmentedViewDelegate,JYSegmentedViewDatasource
extension JYPageController: JYSegmentedViewDelegate, JYSegmentedViewDataSource {
public func numberOfSegmentedViewItems() -> Int {
return childControllersCount
}
public func segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int) -> String {
guard let source = dataSource else {
return ""
}
return source.pageController(self, titleAt: index)
}
public func segmentedView(_ segmentedView: JYSegmentedView, customViewAt index: Int) -> UIView? {
guard let source = dataSource else {
return nil
}
return source.pageController?(self, customViewAt: index)
}
public func segmentedView(_ segmentedView: JYSegmentedView, badgeViewAt index: Int) -> UIView? {
guard let source = dataSource else {
return nil
}
return source.pageController?(self, badgeViewAt: index)
}
public func segmentedView(_ segmentedView: JYSegmentedView, didSelectItemAt index: Int) {
scrollByDragging = false
let cacheKey = String(index) as NSString
let controller = displayControllerCache[cacheKey]
if index < childControllersCount {
if controller == nil {
addChildController(index: index)
}
selectedIndex = index
let contentOffsetX = CGFloat(index)*childControllerViewFrame.size.width
pageContentScrollView.setContentOffset(CGPoint(x: contentOffsetX, y: 0), animated: config.scrollViewAnimationWhenSegmentItemSelected)
delegate?.pageController?(self, didEnterControllerAt: index)
}
}
}
//MARK: - UITableViewDelegate,UITableViewDatasource
extension JYPageController: UITableViewDelegate, UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
// public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
// return menuViewFrame.height
// }
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// return pageContentScrollView.frame.height
// return self.view.frame.size.height
return mainScrollView.frame.height - self.config.hoverOffset
}
// public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// let headerContentView = UIView(frame: CGRect(x: 0, y: 0, width: menuViewFrame.size.width, height: menuViewFrame.size.height))
// headerContentView.backgroundColor = .white
// headerContentView.addSubview(segmentedView)
// return headerContentView
// }
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
cell.backgroundColor = .clear
cell.selectionStyle = .none
// pageContentScrollView.frame = CGRect(x: 0, y: 0, width: childControllerViewFrame.width, height: childControllerViewFrame.height)
cell.contentView.addSubview(segmentedView)
cell.contentView.addSubview(pageContentScrollView)
return cell
}
}