JXSegmentedListContainerView.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. //
  2. // JXSegmentedListContainerView.swift
  3. // JXSegmentedView
  4. //
  5. // Created by jiaxin on 2018/12/26.
  6. // Copyright © 2018 jiaxin. All rights reserved.
  7. //
  8. import UIKit
  9. /// 列表容器视图的类型
  10. ///- ScrollView: UIScrollView。优势:没有其他副作用。劣势:视图内存占用相对大一点。因为所有的列表视图都在UIScrollView的视图层级里面。
  11. /// - CollectionView: 使用UICollectionView。优势:因为列表被添加到cell上,视图的内存占用更少,适合内存要求特别高的场景。劣势:因为cell重用机制的问题,导致列表下拉刷新视图(比如MJRefresh),会因为被removeFromSuperview而被隐藏。所以,列表有下拉刷新需求的,请使用scrollView type。
  12. public enum JXSegmentedListContainerType {
  13. case scrollView
  14. case collectionView
  15. }
  16. @objc
  17. public protocol JXSegmentedListContainerViewListDelegate {
  18. /// 如果列表是VC,就返回VC.view
  19. /// 如果列表是View,就返回View自己
  20. ///
  21. /// - Returns: 返回列表视图
  22. func listView() -> UIView
  23. @objc optional func listWillAppear()
  24. @objc optional func listDidAppear()
  25. @objc optional func listWillDisappear()
  26. @objc optional func listDidDisappear()
  27. }
  28. @objc
  29. public protocol JXSegmentedListContainerViewDataSource {
  30. /// 返回list的数量
  31. func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int
  32. /// 根据index初始化一个对应列表实例,需要是遵从`JXSegmentedListContainerViewListDelegate`协议的对象。
  33. /// 如果列表是用自定义UIView封装的,就让自定义UIView遵从`JXSegmentedListContainerViewListDelegate`协议,该方法返回自定义UIView即可。
  34. /// 如果列表是用自定义UIViewController封装的,就让自定义UIViewController遵从`JXSegmentedListContainerViewListDelegate`协议,该方法返回自定义UIViewController即可。
  35. /// 注意:一定要是新生成的实例!!!
  36. ///
  37. /// - Parameters:
  38. /// - listContainerView: JXSegmentedListContainerView
  39. /// - index: 目标index
  40. /// - Returns: 遵从JXSegmentedListContainerViewListDelegate协议的实例
  41. func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate
  42. /// 控制能否初始化对应index的列表。有些业务需求,需要在某些情况才允许初始化某些列表,通过通过该代理实现控制。
  43. @objc optional func listContainerView(_ listContainerView: JXSegmentedListContainerView, canInitListAt index: Int) -> Bool
  44. /// 返回自定义UIScrollView或UICollectionView的Class
  45. /// 某些特殊情况需要自己处理UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture,需要处理手势相关代理。
  46. ///
  47. /// - Parameter listContainerView: JXSegmentedListContainerView
  48. /// - Returns: 自定义UIScrollView实例
  49. @objc optional func scrollViewClass(in listContainerView: JXSegmentedListContainerView) -> AnyClass
  50. }
  51. open class JXSegmentedListContainerView: UIView, JXSegmentedViewListContainer, JXSegmentedViewRTLCompatible {
  52. open private(set) var type: JXSegmentedListContainerType
  53. open private(set) weak var dataSource: JXSegmentedListContainerViewDataSource?
  54. open private(set) var scrollView: UIScrollView!
  55. /// 已经加载过的列表字典。key是index,value是对应的列表
  56. open private(set) var validListDict = [Int:JXSegmentedListContainerViewListDelegate]()
  57. /// 滚动切换的时候,滚动距离超过一页的多少百分比,就触发列表的初始化。默认0.01(即列表显示了一点就触发加载)。范围0~1,开区间不包括0和1
  58. open var initListPercent: CGFloat = 0.01 {
  59. didSet {
  60. if initListPercent <= 0 || initListPercent >= 1 {
  61. assertionFailure("initListPercent值范围为开区间(0,1),即不包括0和1")
  62. }
  63. }
  64. }
  65. open var listCellBackgroundColor: UIColor = .white
  66. /// 需要和segmentedView.defaultSelectedIndex保持一致,用于触发默认index列表的加载
  67. open var defaultSelectedIndex: Int = 0 {
  68. didSet {
  69. currentIndex = defaultSelectedIndex
  70. }
  71. }
  72. private var currentIndex: Int = 0
  73. private lazy var collectionView: UICollectionView = {
  74. let layout = UICollectionViewFlowLayout()
  75. layout.scrollDirection = .horizontal
  76. layout.minimumLineSpacing = 0
  77. layout.minimumInteritemSpacing = 0
  78. if let collectionViewClass = dataSource?.scrollViewClass?(in: self) as? UICollectionView.Type {
  79. return collectionViewClass.init(frame: CGRect.zero, collectionViewLayout: layout)
  80. }else {
  81. return UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout)
  82. }
  83. }()
  84. private lazy var containerVC = JXSegmentedListContainerViewController()
  85. private var willAppearIndex: Int = -1
  86. private var willDisappearIndex: Int = -1
  87. public init(dataSource: JXSegmentedListContainerViewDataSource, type: JXSegmentedListContainerType = .scrollView) {
  88. self.dataSource = dataSource
  89. self.type = type
  90. super.init(frame: CGRect.zero)
  91. commonInit()
  92. }
  93. required public init?(coder aDecoder: NSCoder) {
  94. fatalError("init(coder:) has not been implemented")
  95. }
  96. open func commonInit() {
  97. containerVC.view.backgroundColor = .clear
  98. addSubview(containerVC.view)
  99. containerVC.viewWillAppearClosure = {[weak self] in
  100. self?.listWillAppear(at: self?.currentIndex ?? 0)
  101. }
  102. containerVC.viewDidAppearClosure = {[weak self] in
  103. self?.listDidAppear(at: self?.currentIndex ?? 0)
  104. }
  105. containerVC.viewWillDisappearClosure = {[weak self] in
  106. self?.listWillDisappear(at: self?.currentIndex ?? 0)
  107. }
  108. containerVC.viewDidDisappearClosure = {[weak self] in
  109. self?.listDidDisappear(at: self?.currentIndex ?? 0)
  110. }
  111. if type == .scrollView {
  112. if let scrollViewClass = dataSource?.scrollViewClass?(in: self) as? UIScrollView.Type {
  113. scrollView = scrollViewClass.init()
  114. }else {
  115. scrollView = UIScrollView.init()
  116. }
  117. scrollView.delegate = self
  118. scrollView.isPagingEnabled = true
  119. scrollView.showsVerticalScrollIndicator = false
  120. scrollView.showsHorizontalScrollIndicator = false
  121. scrollView.scrollsToTop = false
  122. scrollView.bounces = false
  123. if #available(iOS 11.0, *) {
  124. scrollView.contentInsetAdjustmentBehavior = .never
  125. }
  126. if segmentedViewShouldRTLLayout() {
  127. segmentedView(horizontalFlipForView: scrollView)
  128. }
  129. containerVC.view.addSubview(scrollView)
  130. }else if type == .collectionView {
  131. collectionView.isPagingEnabled = true
  132. collectionView.showsHorizontalScrollIndicator = false
  133. collectionView.showsVerticalScrollIndicator = false
  134. collectionView.scrollsToTop = false
  135. collectionView.bounces = false
  136. collectionView.dataSource = self
  137. collectionView.delegate = self
  138. collectionView.register(JXSegmentedRTLCollectionCell.self, forCellWithReuseIdentifier: "cell")
  139. if #available(iOS 10.0, *) {
  140. collectionView.isPrefetchingEnabled = false
  141. }
  142. if #available(iOS 11.0, *) {
  143. self.collectionView.contentInsetAdjustmentBehavior = .never
  144. }
  145. if segmentedViewShouldRTLLayout() {
  146. collectionView.semanticContentAttribute = .forceLeftToRight
  147. segmentedView(horizontalFlipForView: collectionView)
  148. }
  149. containerVC.view.addSubview(collectionView)
  150. //让外部统一访问scrollView
  151. scrollView = collectionView
  152. }
  153. }
  154. open override func willMove(toSuperview newSuperview: UIView?) {
  155. super.willMove(toSuperview: newSuperview)
  156. var next: UIResponder? = newSuperview
  157. while next != nil {
  158. if let vc = next as? UIViewController{
  159. vc.addChild(containerVC)
  160. break
  161. }
  162. next = next?.next
  163. }
  164. }
  165. open override func layoutSubviews() {
  166. super.layoutSubviews()
  167. containerVC.view.frame = bounds
  168. guard let count = dataSource?.numberOfLists(in: self) else {
  169. return
  170. }
  171. if type == .scrollView {
  172. if scrollView.frame == CGRect.zero || scrollView.bounds.size != bounds.size {
  173. scrollView.frame = bounds
  174. scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(count), height: scrollView.bounds.size.height)
  175. for (index, list) in validListDict {
  176. list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
  177. }
  178. scrollView.contentOffset = CGPoint(x: CGFloat(currentIndex)*scrollView.bounds.size.width, y: 0)
  179. }else {
  180. scrollView.frame = bounds
  181. scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(count), height: scrollView.bounds.size.height)
  182. }
  183. }else {
  184. if collectionView.frame == CGRect.zero || collectionView.bounds.size != bounds.size {
  185. collectionView.frame = bounds
  186. collectionView.collectionViewLayout.invalidateLayout()
  187. collectionView.setContentOffset(CGPoint(x: CGFloat(currentIndex)*collectionView.bounds.size.width, y: 0), animated: false)
  188. }else {
  189. collectionView.frame = bounds
  190. }
  191. }
  192. }
  193. //MARK: - JXSegmentedViewListContainer
  194. public func contentScrollView() -> UIScrollView {
  195. return scrollView
  196. }
  197. public func scrolling(from leftIndex: Int, to rightIndex: Int, percent: CGFloat, selectedIndex: Int) {
  198. }
  199. open func didClickSelectedItem(at index: Int) {
  200. guard checkIndexValid(index) else {
  201. return
  202. }
  203. willAppearIndex = -1
  204. willDisappearIndex = -1
  205. if currentIndex != index {
  206. listWillDisappear(at: currentIndex)
  207. listWillAppear(at: index)
  208. listDidDisappear(at: currentIndex)
  209. listDidAppear(at: index)
  210. }
  211. }
  212. open func reloadData() {
  213. guard let dataSource = dataSource else { return }
  214. if currentIndex < 0 || currentIndex >= dataSource.numberOfLists(in: self) {
  215. defaultSelectedIndex = 0
  216. currentIndex = 0
  217. }
  218. validListDict.values.forEach { (list) in
  219. if let listVC = list as? UIViewController {
  220. listVC.removeFromParent()
  221. }
  222. list.listView().removeFromSuperview()
  223. }
  224. validListDict.removeAll()
  225. if type == .scrollView {
  226. scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(dataSource.numberOfLists(in: self)), height: scrollView.bounds.size.height)
  227. }else {
  228. collectionView.reloadData()
  229. }
  230. listWillAppear(at: currentIndex)
  231. listDidAppear(at: currentIndex)
  232. }
  233. //MARK: - Private
  234. func initListIfNeeded(at index: Int) {
  235. guard let dataSource = dataSource else { return }
  236. if dataSource.listContainerView?(self, canInitListAt: index) == false {
  237. return
  238. }
  239. var existedList = validListDict[index]
  240. if existedList != nil {
  241. //列表已经创建好了
  242. return
  243. }
  244. existedList = dataSource.listContainerView(self, initListAt: index)
  245. guard let list = existedList else {
  246. return
  247. }
  248. if let vc = list as? UIViewController {
  249. containerVC.addChild(vc)
  250. }
  251. validListDict[index] = list
  252. if type == .scrollView {
  253. list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
  254. scrollView.addSubview(list.listView())
  255. if segmentedViewShouldRTLLayout() {
  256. segmentedView(horizontalFlipForView: list.listView())
  257. }
  258. }else {
  259. let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0))
  260. cell?.contentView.subviews.forEach { $0.removeFromSuperview() }
  261. list.listView().frame = cell?.contentView.bounds ?? CGRect.zero
  262. cell?.contentView.addSubview(list.listView())
  263. }
  264. }
  265. private func listWillAppear(at index: Int) {
  266. guard let dataSource = dataSource else { return }
  267. guard checkIndexValid(index) else {
  268. return
  269. }
  270. var existedList = validListDict[index]
  271. if existedList != nil {
  272. existedList?.listWillAppear?()
  273. if let vc = existedList as? UIViewController {
  274. vc.beginAppearanceTransition(true, animated: false)
  275. }
  276. }else {
  277. //当前列表未被创建(页面初始化或通过点击触发的listWillAppear)
  278. guard dataSource.listContainerView?(self, canInitListAt: index) != false else {
  279. return
  280. }
  281. existedList = dataSource.listContainerView(self, initListAt: index)
  282. guard let list = existedList else {
  283. return
  284. }
  285. if let vc = list as? UIViewController {
  286. containerVC.addChild(vc)
  287. }
  288. validListDict[index] = list
  289. if type == .scrollView {
  290. if list.listView().superview == nil {
  291. list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
  292. scrollView.addSubview(list.listView())
  293. if segmentedViewShouldRTLLayout() {
  294. segmentedView(horizontalFlipForView: list.listView())
  295. }
  296. }
  297. list.listWillAppear?()
  298. if let vc = list as? UIViewController {
  299. vc.beginAppearanceTransition(true, animated: false)
  300. }
  301. }else {
  302. let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0))
  303. cell?.contentView.subviews.forEach { $0.removeFromSuperview() }
  304. list.listView().frame = cell?.contentView.bounds ?? CGRect.zero
  305. cell?.contentView.addSubview(list.listView())
  306. list.listWillAppear?()
  307. if let vc = list as? UIViewController {
  308. vc.beginAppearanceTransition(true, animated: false)
  309. }
  310. }
  311. }
  312. }
  313. private func listDidAppear(at index: Int) {
  314. guard checkIndexValid(index) else {
  315. return
  316. }
  317. currentIndex = index
  318. let list = validListDict[index]
  319. list?.listDidAppear?()
  320. if let vc = list as? UIViewController {
  321. vc.endAppearanceTransition()
  322. }
  323. }
  324. private func listWillDisappear(at index: Int) {
  325. guard checkIndexValid(index) else {
  326. return
  327. }
  328. let list = validListDict[index]
  329. list?.listWillDisappear?()
  330. if let vc = list as? UIViewController {
  331. vc.beginAppearanceTransition(false, animated: false)
  332. }
  333. }
  334. private func listDidDisappear(at index: Int) {
  335. guard checkIndexValid(index) else {
  336. return
  337. }
  338. let list = validListDict[index]
  339. list?.listDidDisappear?()
  340. if let vc = list as? UIViewController {
  341. vc.endAppearanceTransition()
  342. }
  343. }
  344. private func checkIndexValid(_ index: Int) -> Bool {
  345. guard let dataSource = dataSource else { return false }
  346. let count = dataSource.numberOfLists(in: self)
  347. if count <= 0 || index >= count {
  348. return false
  349. }
  350. return true
  351. }
  352. private func listDidAppearOrDisappear(scrollView: UIScrollView) {
  353. let currentIndexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width
  354. if willAppearIndex != -1 || willDisappearIndex != -1 {
  355. let disappearIndex = willDisappearIndex
  356. let appearIndex = willAppearIndex
  357. if willAppearIndex > willDisappearIndex {
  358. //将要出现的列表在右边
  359. if currentIndexPercent >= CGFloat(willAppearIndex) {
  360. willDisappearIndex = -1
  361. willAppearIndex = -1
  362. listDidDisappear(at: disappearIndex)
  363. listDidAppear(at: appearIndex)
  364. }
  365. }else {
  366. //将要出现的列表在左边
  367. if currentIndexPercent <= CGFloat(willAppearIndex) {
  368. willDisappearIndex = -1
  369. willAppearIndex = -1
  370. listDidDisappear(at: disappearIndex)
  371. listDidAppear(at: appearIndex)
  372. }
  373. }
  374. }
  375. }
  376. }
  377. extension JXSegmentedListContainerView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
  378. public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  379. guard let dataSource = dataSource else { return 0 }
  380. return dataSource.numberOfLists(in: self)
  381. }
  382. public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  383. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
  384. cell.contentView.backgroundColor = listCellBackgroundColor
  385. cell.contentView.subviews.forEach { $0.removeFromSuperview() }
  386. let list = validListDict[indexPath.item]
  387. if list != nil {
  388. list?.listView().frame = cell.contentView.bounds
  389. cell.contentView.addSubview(list!.listView())
  390. }
  391. return cell
  392. }
  393. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
  394. return bounds.size
  395. }
  396. public func scrollViewDidScroll(_ scrollView: UIScrollView) {
  397. guard scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating else {
  398. return
  399. }
  400. let percent = scrollView.contentOffset.x/scrollView.bounds.size.width
  401. let maxCount = Int(round(scrollView.contentSize.width/scrollView.bounds.size.width))
  402. var leftIndex = Int(floor(Double(percent)))
  403. leftIndex = max(0, min(maxCount - 1, leftIndex))
  404. let rightIndex = leftIndex + 1;
  405. if percent < 0 || rightIndex >= maxCount {
  406. listDidAppearOrDisappear(scrollView: scrollView)
  407. return
  408. }
  409. let remainderRatio = percent - CGFloat(leftIndex)
  410. if rightIndex == currentIndex {
  411. //当前选中的在右边,用户正在从右边往左边滑动
  412. if validListDict[leftIndex] == nil && remainderRatio < (1 - initListPercent) {
  413. initListIfNeeded(at: leftIndex)
  414. }else if validListDict[leftIndex] != nil {
  415. if willAppearIndex == -1 {
  416. willAppearIndex = leftIndex;
  417. listWillAppear(at: willAppearIndex)
  418. }
  419. }
  420. if willDisappearIndex == -1 {
  421. willDisappearIndex = rightIndex
  422. listWillDisappear(at: willDisappearIndex)
  423. }
  424. }else {
  425. //当前选中的在左边,用户正在从左边往右边滑动
  426. if validListDict[rightIndex] == nil && remainderRatio > initListPercent {
  427. initListIfNeeded(at: rightIndex)
  428. }else if validListDict[rightIndex] != nil {
  429. if willAppearIndex == -1 {
  430. willAppearIndex = rightIndex
  431. listWillAppear(at: willAppearIndex)
  432. }
  433. }
  434. if willDisappearIndex == -1 {
  435. willDisappearIndex = leftIndex
  436. listWillDisappear(at: willDisappearIndex)
  437. }
  438. }
  439. listDidAppearOrDisappear(scrollView: scrollView)
  440. }
  441. public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  442. //滑动到一半又取消滑动处理
  443. if willAppearIndex != -1 && willDisappearIndex != -1 {
  444. listWillDisappear(at: willAppearIndex)
  445. listWillAppear(at: willDisappearIndex)
  446. listDidDisappear(at: willAppearIndex)
  447. listDidAppear(at: willDisappearIndex)
  448. willDisappearIndex = -1
  449. willAppearIndex = -1
  450. }
  451. }
  452. }
  453. class JXSegmentedListContainerViewController: UIViewController {
  454. var viewWillAppearClosure: (()->())?
  455. var viewDidAppearClosure: (()->())?
  456. var viewWillDisappearClosure: (()->())?
  457. var viewDidDisappearClosure: (()->())?
  458. override var shouldAutomaticallyForwardAppearanceMethods: Bool { return false }
  459. override func viewWillAppear(_ animated: Bool) {
  460. super.viewWillAppear(animated)
  461. viewWillAppearClosure?()
  462. }
  463. override func viewDidAppear(_ animated: Bool) {
  464. super.viewDidAppear(animated)
  465. viewDidAppearClosure?()
  466. }
  467. override func viewWillDisappear(_ animated: Bool) {
  468. super.viewWillDisappear(animated)
  469. viewWillDisappearClosure?()
  470. }
  471. override func viewDidDisappear(_ animated: Bool) {
  472. super.viewDidDisappear(animated)
  473. viewDidDisappearClosure?()
  474. }
  475. }