// // JXSegmentedListContainerView.swift // JXSegmentedView // // Created by jiaxin on 2018/12/26. // Copyright © 2018 jiaxin. All rights reserved. // import UIKit /// 列表容器视图的类型 ///- ScrollView: UIScrollView。优势:没有其他副作用。劣势:视图内存占用相对大一点。因为所有的列表视图都在UIScrollView的视图层级里面。 /// - CollectionView: 使用UICollectionView。优势:因为列表被添加到cell上,视图的内存占用更少,适合内存要求特别高的场景。劣势:因为cell重用机制的问题,导致列表下拉刷新视图(比如MJRefresh),会因为被removeFromSuperview而被隐藏。所以,列表有下拉刷新需求的,请使用scrollView type。 public enum JXSegmentedListContainerType { case scrollView case collectionView } @objc public protocol JXSegmentedListContainerViewListDelegate { /// 如果列表是VC,就返回VC.view /// 如果列表是View,就返回View自己 /// /// - Returns: 返回列表视图 func listView() -> UIView @objc optional func listWillAppear() @objc optional func listDidAppear() @objc optional func listWillDisappear() @objc optional func listDidDisappear() } @objc public protocol JXSegmentedListContainerViewDataSource { /// 返回list的数量 func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int /// 根据index初始化一个对应列表实例,需要是遵从`JXSegmentedListContainerViewListDelegate`协议的对象。 /// 如果列表是用自定义UIView封装的,就让自定义UIView遵从`JXSegmentedListContainerViewListDelegate`协议,该方法返回自定义UIView即可。 /// 如果列表是用自定义UIViewController封装的,就让自定义UIViewController遵从`JXSegmentedListContainerViewListDelegate`协议,该方法返回自定义UIViewController即可。 /// 注意:一定要是新生成的实例!!! /// /// - Parameters: /// - listContainerView: JXSegmentedListContainerView /// - index: 目标index /// - Returns: 遵从JXSegmentedListContainerViewListDelegate协议的实例 func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate /// 控制能否初始化对应index的列表。有些业务需求,需要在某些情况才允许初始化某些列表,通过通过该代理实现控制。 @objc optional func listContainerView(_ listContainerView: JXSegmentedListContainerView, canInitListAt index: Int) -> Bool /// 返回自定义UIScrollView或UICollectionView的Class /// 某些特殊情况需要自己处理UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture,需要处理手势相关代理。 /// /// - Parameter listContainerView: JXSegmentedListContainerView /// - Returns: 自定义UIScrollView实例 @objc optional func scrollViewClass(in listContainerView: JXSegmentedListContainerView) -> AnyClass } open class JXSegmentedListContainerView: UIView, JXSegmentedViewListContainer, JXSegmentedViewRTLCompatible { open private(set) var type: JXSegmentedListContainerType open private(set) weak var dataSource: JXSegmentedListContainerViewDataSource? open private(set) var scrollView: UIScrollView! /// 已经加载过的列表字典。key是index,value是对应的列表 open private(set) var validListDict = [Int:JXSegmentedListContainerViewListDelegate]() /// 滚动切换的时候,滚动距离超过一页的多少百分比,就触发列表的初始化。默认0.01(即列表显示了一点就触发加载)。范围0~1,开区间不包括0和1 open var initListPercent: CGFloat = 0.01 { didSet { if initListPercent <= 0 || initListPercent >= 1 { assertionFailure("initListPercent值范围为开区间(0,1),即不包括0和1") } } } open var listCellBackgroundColor: UIColor = .white /// 需要和segmentedView.defaultSelectedIndex保持一致,用于触发默认index列表的加载 open var defaultSelectedIndex: Int = 0 { didSet { currentIndex = defaultSelectedIndex } } private var currentIndex: Int = 0 private lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 if let collectionViewClass = dataSource?.scrollViewClass?(in: self) as? UICollectionView.Type { return collectionViewClass.init(frame: CGRect.zero, collectionViewLayout: layout) }else { return UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout) } }() private lazy var containerVC = JXSegmentedListContainerViewController() private var willAppearIndex: Int = -1 private var willDisappearIndex: Int = -1 public init(dataSource: JXSegmentedListContainerViewDataSource, type: JXSegmentedListContainerType = .scrollView) { self.dataSource = dataSource self.type = type super.init(frame: CGRect.zero) commonInit() } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } open func commonInit() { containerVC.view.backgroundColor = .clear addSubview(containerVC.view) containerVC.viewWillAppearClosure = {[weak self] in self?.listWillAppear(at: self?.currentIndex ?? 0) } containerVC.viewDidAppearClosure = {[weak self] in self?.listDidAppear(at: self?.currentIndex ?? 0) } containerVC.viewWillDisappearClosure = {[weak self] in self?.listWillDisappear(at: self?.currentIndex ?? 0) } containerVC.viewDidDisappearClosure = {[weak self] in self?.listDidDisappear(at: self?.currentIndex ?? 0) } if type == .scrollView { if let scrollViewClass = dataSource?.scrollViewClass?(in: self) as? UIScrollView.Type { scrollView = scrollViewClass.init() }else { scrollView = UIScrollView.init() } scrollView.delegate = self scrollView.isPagingEnabled = true scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false scrollView.scrollsToTop = false scrollView.bounces = false if #available(iOS 11.0, *) { scrollView.contentInsetAdjustmentBehavior = .never } if segmentedViewShouldRTLLayout() { segmentedView(horizontalFlipForView: scrollView) } containerVC.view.addSubview(scrollView) }else if type == .collectionView { collectionView.isPagingEnabled = true collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false collectionView.scrollsToTop = false collectionView.bounces = false collectionView.dataSource = self collectionView.delegate = self collectionView.register(JXSegmentedRTLCollectionCell.self, forCellWithReuseIdentifier: "cell") if #available(iOS 10.0, *) { collectionView.isPrefetchingEnabled = false } if #available(iOS 11.0, *) { self.collectionView.contentInsetAdjustmentBehavior = .never } if segmentedViewShouldRTLLayout() { collectionView.semanticContentAttribute = .forceLeftToRight segmentedView(horizontalFlipForView: collectionView) } containerVC.view.addSubview(collectionView) //让外部统一访问scrollView scrollView = collectionView } } open override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) var next: UIResponder? = newSuperview while next != nil { if let vc = next as? UIViewController{ vc.addChild(containerVC) break } next = next?.next } } open override func layoutSubviews() { super.layoutSubviews() containerVC.view.frame = bounds guard let count = dataSource?.numberOfLists(in: self) else { return } if type == .scrollView { if scrollView.frame == CGRect.zero || scrollView.bounds.size != bounds.size { scrollView.frame = bounds scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(count), height: scrollView.bounds.size.height) for (index, list) in validListDict { list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height) } scrollView.contentOffset = CGPoint(x: CGFloat(currentIndex)*scrollView.bounds.size.width, y: 0) }else { scrollView.frame = bounds scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(count), height: scrollView.bounds.size.height) } }else { if collectionView.frame == CGRect.zero || collectionView.bounds.size != bounds.size { collectionView.frame = bounds collectionView.collectionViewLayout.invalidateLayout() collectionView.setContentOffset(CGPoint(x: CGFloat(currentIndex)*collectionView.bounds.size.width, y: 0), animated: false) }else { collectionView.frame = bounds } } } //MARK: - JXSegmentedViewListContainer public func contentScrollView() -> UIScrollView { return scrollView } public func scrolling(from leftIndex: Int, to rightIndex: Int, percent: CGFloat, selectedIndex: Int) { } open func didClickSelectedItem(at index: Int) { guard checkIndexValid(index) else { return } willAppearIndex = -1 willDisappearIndex = -1 if currentIndex != index { listWillDisappear(at: currentIndex) listWillAppear(at: index) listDidDisappear(at: currentIndex) listDidAppear(at: index) } } open func reloadData() { guard let dataSource = dataSource else { return } if currentIndex < 0 || currentIndex >= dataSource.numberOfLists(in: self) { defaultSelectedIndex = 0 currentIndex = 0 } validListDict.values.forEach { (list) in if let listVC = list as? UIViewController { listVC.removeFromParent() } list.listView().removeFromSuperview() } validListDict.removeAll() if type == .scrollView { scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(dataSource.numberOfLists(in: self)), height: scrollView.bounds.size.height) }else { collectionView.reloadData() } listWillAppear(at: currentIndex) listDidAppear(at: currentIndex) } //MARK: - Private func initListIfNeeded(at index: Int) { guard let dataSource = dataSource else { return } if dataSource.listContainerView?(self, canInitListAt: index) == false { return } var existedList = validListDict[index] if existedList != nil { //列表已经创建好了 return } existedList = dataSource.listContainerView(self, initListAt: index) guard let list = existedList else { return } if let vc = list as? UIViewController { containerVC.addChild(vc) } validListDict[index] = list if type == .scrollView { list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height) scrollView.addSubview(list.listView()) if segmentedViewShouldRTLLayout() { segmentedView(horizontalFlipForView: list.listView()) } }else { let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) cell?.contentView.subviews.forEach { $0.removeFromSuperview() } list.listView().frame = cell?.contentView.bounds ?? CGRect.zero cell?.contentView.addSubview(list.listView()) } } private func listWillAppear(at index: Int) { guard let dataSource = dataSource else { return } guard checkIndexValid(index) else { return } var existedList = validListDict[index] if existedList != nil { existedList?.listWillAppear?() if let vc = existedList as? UIViewController { vc.beginAppearanceTransition(true, animated: false) } }else { //当前列表未被创建(页面初始化或通过点击触发的listWillAppear) guard dataSource.listContainerView?(self, canInitListAt: index) != false else { return } existedList = dataSource.listContainerView(self, initListAt: index) guard let list = existedList else { return } if let vc = list as? UIViewController { containerVC.addChild(vc) } validListDict[index] = list if type == .scrollView { if list.listView().superview == nil { list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height) scrollView.addSubview(list.listView()) if segmentedViewShouldRTLLayout() { segmentedView(horizontalFlipForView: list.listView()) } } list.listWillAppear?() if let vc = list as? UIViewController { vc.beginAppearanceTransition(true, animated: false) } }else { let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) cell?.contentView.subviews.forEach { $0.removeFromSuperview() } list.listView().frame = cell?.contentView.bounds ?? CGRect.zero cell?.contentView.addSubview(list.listView()) list.listWillAppear?() if let vc = list as? UIViewController { vc.beginAppearanceTransition(true, animated: false) } } } } private func listDidAppear(at index: Int) { guard checkIndexValid(index) else { return } currentIndex = index let list = validListDict[index] list?.listDidAppear?() if let vc = list as? UIViewController { vc.endAppearanceTransition() } } private func listWillDisappear(at index: Int) { guard checkIndexValid(index) else { return } let list = validListDict[index] list?.listWillDisappear?() if let vc = list as? UIViewController { vc.beginAppearanceTransition(false, animated: false) } } private func listDidDisappear(at index: Int) { guard checkIndexValid(index) else { return } let list = validListDict[index] list?.listDidDisappear?() if let vc = list as? UIViewController { vc.endAppearanceTransition() } } private func checkIndexValid(_ index: Int) -> Bool { guard let dataSource = dataSource else { return false } let count = dataSource.numberOfLists(in: self) if count <= 0 || index >= count { return false } return true } private func listDidAppearOrDisappear(scrollView: UIScrollView) { let currentIndexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width if willAppearIndex != -1 || willDisappearIndex != -1 { let disappearIndex = willDisappearIndex let appearIndex = willAppearIndex if willAppearIndex > willDisappearIndex { //将要出现的列表在右边 if currentIndexPercent >= CGFloat(willAppearIndex) { willDisappearIndex = -1 willAppearIndex = -1 listDidDisappear(at: disappearIndex) listDidAppear(at: appearIndex) } }else { //将要出现的列表在左边 if currentIndexPercent <= CGFloat(willAppearIndex) { willDisappearIndex = -1 willAppearIndex = -1 listDidDisappear(at: disappearIndex) listDidAppear(at: appearIndex) } } } } } extension JXSegmentedListContainerView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { guard let dataSource = dataSource else { return 0 } return dataSource.numberOfLists(in: self) } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) cell.contentView.backgroundColor = listCellBackgroundColor cell.contentView.subviews.forEach { $0.removeFromSuperview() } let list = validListDict[indexPath.item] if list != nil { list?.listView().frame = cell.contentView.bounds cell.contentView.addSubview(list!.listView()) } return cell } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return bounds.size } public func scrollViewDidScroll(_ scrollView: UIScrollView) { guard scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating else { return } let percent = scrollView.contentOffset.x/scrollView.bounds.size.width let maxCount = Int(round(scrollView.contentSize.width/scrollView.bounds.size.width)) var leftIndex = Int(floor(Double(percent))) leftIndex = max(0, min(maxCount - 1, leftIndex)) let rightIndex = leftIndex + 1; if percent < 0 || rightIndex >= maxCount { listDidAppearOrDisappear(scrollView: scrollView) return } let remainderRatio = percent - CGFloat(leftIndex) if rightIndex == currentIndex { //当前选中的在右边,用户正在从右边往左边滑动 if validListDict[leftIndex] == nil && remainderRatio < (1 - initListPercent) { initListIfNeeded(at: leftIndex) }else if validListDict[leftIndex] != nil { if willAppearIndex == -1 { willAppearIndex = leftIndex; listWillAppear(at: willAppearIndex) } } if willDisappearIndex == -1 { willDisappearIndex = rightIndex listWillDisappear(at: willDisappearIndex) } }else { //当前选中的在左边,用户正在从左边往右边滑动 if validListDict[rightIndex] == nil && remainderRatio > initListPercent { initListIfNeeded(at: rightIndex) }else if validListDict[rightIndex] != nil { if willAppearIndex == -1 { willAppearIndex = rightIndex listWillAppear(at: willAppearIndex) } } if willDisappearIndex == -1 { willDisappearIndex = leftIndex listWillDisappear(at: willDisappearIndex) } } listDidAppearOrDisappear(scrollView: scrollView) } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { //滑动到一半又取消滑动处理 if willAppearIndex != -1 && willDisappearIndex != -1 { listWillDisappear(at: willAppearIndex) listWillAppear(at: willDisappearIndex) listDidDisappear(at: willAppearIndex) listDidAppear(at: willDisappearIndex) willDisappearIndex = -1 willAppearIndex = -1 } } } class JXSegmentedListContainerViewController: UIViewController { var viewWillAppearClosure: (()->())? var viewDidAppearClosure: (()->())? var viewWillDisappearClosure: (()->())? var viewDidDisappearClosure: (()->())? override var shouldAutomaticallyForwardAppearanceMethods: Bool { return false } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) viewWillAppearClosure?() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) viewDidAppearClosure?() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) viewWillDisappearClosure?() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) viewDidDisappearClosure?() } }