JXSegmentedListContainerView.swift 20 KB

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