XLPageViewController.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. //
  2. // XLPageViewController.m
  3. // XLPageViewControllerExample
  4. //
  5. // Created by MengXianLiang on 2019/5/6.
  6. // Copyright © 2019 xianliang meng. All rights reserved.
  7. // https://github.com/mengxianliang/XLPageViewController
  8. #import "XLPageViewController.h"
  9. #import "XLPageBasicTitleView.h"
  10. #import "XLPageSegmentedTitleView.h"
  11. #import "WFCUUtilities.h"
  12. //调用setViewControllers方法时,可能会用到延时
  13. static float SetViewControllersMethodDelay = 0.1;
  14. typedef void(^XLContentScollBlock)(BOOL scrollEnabled);
  15. @interface XLPageContentView : UIView
  16. @property (nonatomic, strong) XLContentScollBlock scrollBlock;
  17. @end
  18. @implementation XLPageContentView
  19. //兼容和子view滚动冲突问题
  20. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  21. UIView *view = [super hitTest:point withEvent:event];
  22. BOOL pageViewScrollEnabled = !view.xl_letMeScrollFirst;
  23. if (self.scrollBlock) {
  24. self.scrollBlock(pageViewScrollEnabled);
  25. }
  26. return view;
  27. }
  28. @end
  29. @interface XLPageViewController ()<UIPageViewControllerDelegate, UIPageViewControllerDataSource,UIScrollViewDelegate,XLPageTitleViewDataSrouce,XLPageTitleViewDelegate>
  30. //所有的子视图,都加载在contentView上
  31. @property (nonatomic, strong) XLPageContentView *contentView;
  32. //标题
  33. @property (nonatomic, strong) XLPageBasicTitleView *titleView;
  34. //分页控制器
  35. @property (nonatomic, strong) UIPageViewController *pageVC;
  36. //ScrollView
  37. @property (nonatomic, strong) UIScrollView *scrollView;
  38. //显示过的vc数组,用于试图控制器缓存
  39. @property (nonatomic, strong) NSMutableArray *shownVCArr;
  40. //所有标题集合
  41. @property (nonatomic, strong) NSMutableArray *allTitleArr;
  42. //是否加载了pageVC
  43. @property (nonatomic, assign) BOOL pageVCDidLoad;
  44. //判断pageVC是否在切换中
  45. @property (nonatomic, assign) BOOL pageVCAnimating;
  46. //当前配置信息
  47. @property (nonatomic, strong) XLPageViewControllerConfig *config;
  48. //上一次代理返回的index
  49. @property (nonatomic, assign) NSInteger lastDelegateIndex;
  50. //手指拖拽距离
  51. @property (nonatomic, assign) CGFloat dragStartX;
  52. @end
  53. @implementation XLPageViewController
  54. #pragma mark -
  55. #pragma mark 初始化方法
  56. //初始化需要使用initWithConfig方法
  57. - (instancetype)init {
  58. if (self = [super init]) {
  59. [NSException raise:@"Do not use this method" format:@"Must be initialized by initWithConfig"];
  60. }
  61. return self;
  62. }
  63. - (instancetype)initWithConfig:(XLPageViewControllerConfig *)config {
  64. if (self = [super init]) {
  65. [self initUIWithConfig:config];
  66. [self initData];
  67. }
  68. return self;
  69. }
  70. - (void)initUIWithConfig:(XLPageViewControllerConfig *)config {
  71. //保存配置
  72. self.config = config;
  73. //创建contentview
  74. self.contentView = [[XLPageContentView alloc] init];
  75. [self.view addSubview:self.contentView];
  76. __weak typeof(self)weakSelf = self;
  77. self.contentView.scrollBlock = ^(BOOL scrollEnabled) {
  78. if (weakSelf.scrollEnabled) {
  79. weakSelf.scrollView.scrollEnabled = scrollEnabled;
  80. }
  81. };
  82. //防止Navigation引起的缩进
  83. UIView *topView = [[UIView alloc] init];
  84. [self.contentView addSubview:topView];
  85. //创建标题
  86. self.titleView = [[XLPageBasicTitleView alloc] initWithConfig:config];
  87. if (config.titleViewStyle == XLPageTitleViewStyleSegmented) {
  88. self.titleView = [[XLPageSegmentedTitleView alloc] initWithConfig:config];
  89. }
  90. self.titleView.dataSource = self;
  91. self.titleView.delegate = self;
  92. [self.contentView addSubview:self.titleView];
  93. //创建PageVC
  94. self.pageVC = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
  95. self.pageVC.delegate = self;
  96. self.pageVC.dataSource = self;
  97. [self.contentView addSubview:self.pageVC.view];
  98. [self addChildViewController:self.pageVC];
  99. //设置ScrollView代理
  100. for (UIScrollView *scrollView in self.pageVC.view.subviews) {
  101. if ([scrollView isKindOfClass:[UIScrollView class]]) {
  102. self.scrollView = scrollView;
  103. self.scrollView.delegate = self;
  104. }
  105. }
  106. //默认可以滚动
  107. self.scrollEnabled = YES;
  108. //默认打开自动回答弹
  109. self.bounces = YES;
  110. //初始化上一次返回的index
  111. self.lastDelegateIndex = -1;
  112. //兼容全屏返回手势识别
  113. self.scrollView.xl_otherGestureRecognizerBlock = ^BOOL(UIGestureRecognizer * _Nonnull otherGestureRecognizer) {
  114. return weakSelf.selectedIndex == 0 && [weakSelf.respondOtherGestureDelegateClassList containsObject:NSStringFromClass(otherGestureRecognizer.delegate.class)];
  115. };
  116. }
  117. //初始化vc缓存数组
  118. - (void)initData {
  119. self.shownVCArr = [[NSMutableArray alloc] init];
  120. }
  121. //设置titleView位置
  122. - (void)viewWillAppear:(BOOL)animated {
  123. [super viewWillAppear:animated];
  124. if (self.config.showTitleInNavigationBar) {
  125. self.parentViewController.navigationItem.titleView = self.titleView;
  126. }
  127. }
  128. - (void)viewDidLayoutSubviews {
  129. [super viewDidLayoutSubviews];
  130. //更新contentview位置
  131. self.contentView.frame = self.view.bounds;
  132. //更新标题位置
  133. self.titleView.frame = CGRectMake(0, 0, self.contentView.bounds.size.width, self.config.titleViewHeight);
  134. //更新pageVC位置
  135. self.pageVC.view.frame = CGRectMake(0, self.config.titleViewHeight, self.contentView.bounds.size.width, self.contentView.bounds.size.height - self.config.titleViewHeight);
  136. if (self.config.showTitleInNavigationBar) {
  137. self.pageVC.view.frame = self.contentView.bounds;
  138. }
  139. //自动选中当前位置_selectedIndex
  140. if (!self.pageVCDidLoad) {
  141. //设置加载标记为已加载
  142. [self switchToViewControllerAdIndex:_selectedIndex animated:NO];
  143. self.pageVCDidLoad = YES;
  144. }
  145. //初始化标题数组
  146. self.allTitleArr = [[NSMutableArray alloc] init];
  147. for (NSInteger i = 0; i < [self numberOfPage]; i++) {
  148. [self.allTitleArr addObject:[self titleForIndex:i]];
  149. }
  150. }
  151. #pragma mark -
  152. #pragma mark UIPageViewControllerDelegate
  153. //滚动切换时调用
  154. - (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers {
  155. self.pageVCAnimating = YES;
  156. }
  157. //滚动切换时调用
  158. - (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers transitionCompleted:(BOOL)completed {
  159. if (!completed) {
  160. //切换中属性更新
  161. self.pageVCAnimating = NO;
  162. return;
  163. }
  164. //更新选中位置
  165. UIViewController *vc = self.pageVC.viewControllers.firstObject;
  166. _selectedIndex = [self.allTitleArr indexOfObject:vc.xl_title];
  167. //标题居中
  168. self.titleView.selectedIndex = _selectedIndex;
  169. //回调代理方法
  170. [self delegateSelectedAdIndex:_selectedIndex];
  171. //切换中属性更新
  172. self.pageVCAnimating = NO;
  173. }
  174. #pragma mark -
  175. #pragma mark UIPageViewControllerDataSource
  176. - (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
  177. //修正因为拖拽距离过大,导致空白界面问题
  178. [self fixSelectedIndexWhenDragingBefore];
  179. return [self viewControllerForIndex:_selectedIndex - 1];
  180. }
  181. - (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
  182. //修正因为拖拽距离过大,导致空白界面问题
  183. [self fixSelectedIndexWhenDragingAfter];
  184. return [self viewControllerForIndex:_selectedIndex + 1];
  185. }
  186. //修正因拖拽距离过大,导致出现空白界面问题
  187. - (void)fixSelectedIndexWhenDragingBefore {
  188. //计算手动拖拽的距离,避免快速滑动时触发以下算法
  189. CGFloat dragDistance = fabs(self.scrollView.contentOffset.x - self.dragStartX);
  190. //判断1、是否是手指正在拖拽 2、是否滑动距离够大。如果两者成立,则表示需要手动修正位置
  191. if (self.scrollView.isTracking && dragDistance > self.scrollView.bounds.size.width) {
  192. self.pageVCAnimating = NO;
  193. NSInteger targetIndex = _selectedIndex - 1;
  194. targetIndex = targetIndex < 0 ? 0 : targetIndex;
  195. self.selectedIndex = targetIndex;
  196. self.titleView.stopAnimation = NO;
  197. //执行代理方法
  198. [self delegateSelectedAdIndex:targetIndex];
  199. }
  200. }
  201. //修正因拖拽距离过大,导致出现空白界面问题
  202. - (void)fixSelectedIndexWhenDragingAfter {
  203. //计算手动拖拽的距离,避免快速滑动时触发以下算法
  204. CGFloat dragDistance = fabs(self.scrollView.contentOffset.x - self.dragStartX);
  205. //判断1、是否是手指正在拖拽 2、是否滑动距离够大。如果两者成立,则表示需要手动修正位置
  206. if (self.scrollView.isTracking && dragDistance > self.scrollView.bounds.size.width) {
  207. self.pageVCAnimating = NO;
  208. NSInteger targetIndex = _selectedIndex + 1;
  209. targetIndex = targetIndex >= [self numberOfPage] ? [self numberOfPage] - 1 : targetIndex;
  210. self.selectedIndex = targetIndex;
  211. self.titleView.stopAnimation = NO;
  212. //执行代理方法
  213. [self delegateSelectedAdIndex:targetIndex];
  214. }
  215. }
  216. #pragma mark -
  217. #pragma mark ScrollViewDelegate
  218. //滚动时计算标题动画进度
  219. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  220. CGFloat value = scrollView.contentOffset.x - scrollView.bounds.size.width;
  221. self.titleView.animationProgress = value/scrollView.bounds.size.width;
  222. //设置拖拽手势问题
  223. if (self.respondOtherGestureDelegateClassList.count > 0) {
  224. BOOL scrollDisababled = value < 0 && self.selectedIndex == 0 && self.respondOtherGestureDelegateClassList.count;
  225. scrollView.scrollEnabled = !scrollDisababled;
  226. }
  227. //设置边缘回弹问题
  228. BOOL dragToTheEdge = (self.selectedIndex == 0 && value < 0) || (self.selectedIndex == [self numberOfPage] - 1 && value > 0);
  229. if (dragToTheEdge && scrollView.isDragging && !self.bounces) {
  230. scrollView.scrollEnabled = NO;
  231. }
  232. }
  233. //更新执行动画状态
  234. - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
  235. self.titleView.stopAnimation = false;
  236. }
  237. //更新执行动画状态
  238. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  239. self.titleView.stopAnimation = false;
  240. }
  241. //更新执行动画状态
  242. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  243. self.titleView.stopAnimation = false;
  244. //保存手指拖拽起始位置
  245. self.dragStartX = scrollView.contentOffset.x;
  246. }
  247. //更新执行动画状态
  248. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  249. self.titleView.stopAnimation = false;
  250. scrollView.scrollEnabled = self.scrollEnabled;
  251. }
  252. #pragma mark -
  253. #pragma mark PageTitleViewDataSource&Delegate
  254. //titleview数据源方法
  255. - (NSInteger)pageTitleViewNumberOfTitle {
  256. return [self numberOfPage];
  257. }
  258. //titleview数据源方法
  259. - (NSString *)pageTitleViewTitleForIndex:(NSInteger)index {
  260. return [self titleForIndex:index];
  261. }
  262. //titleview数据源方法
  263. - (XLPageTitleCell *)pageTitleViewCellForItemAtIndex:(NSInteger)index {
  264. if ([self.dataSource respondsToSelector:@selector(pageViewController:titleViewCellForItemAtIndex:)]) {
  265. return [self.dataSource pageViewController:self titleViewCellForItemAtIndex:index];
  266. }
  267. return nil;
  268. }
  269. //titleview代理方法
  270. - (BOOL)pageTitleViewDidSelectedAtIndex:(NSInteger)index {
  271. BOOL switchSuccess = [self switchToViewControllerAdIndex:index animated:YES];
  272. if (!switchSuccess) {
  273. return false;
  274. }
  275. self.titleView.stopAnimation = true;
  276. //点击标题切换时会延时加载,所以要延时回调代理方法,避免出现获取当前VC不正确问题
  277. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SetViewControllersMethodDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  278. [self delegateSelectedAdIndex:index];
  279. });
  280. return true;
  281. }
  282. #pragma mark -
  283. #pragma mark Setter
  284. //设置选中位置
  285. - (void)setSelectedIndex:(NSInteger)selectedIndex {
  286. //范围越界,抛出异常
  287. if (selectedIndex < 0 || selectedIndex > [self numberOfPage]) {
  288. [NSException raise:@"selectedIndex is not right !!!" format:@"It is out of range"];
  289. }
  290. BOOL switchSuccess = [self switchToViewControllerAdIndex:selectedIndex animated:YES];
  291. if (!switchSuccess) {return;}
  292. self.titleView.stopAnimation = true;
  293. //更新代理反馈的index
  294. self.lastDelegateIndex = selectedIndex;
  295. }
  296. //滑动开关
  297. - (void)setScrollEnabled:(BOOL)scrollEnabled {
  298. _scrollEnabled = scrollEnabled;
  299. self.scrollView.scrollEnabled = scrollEnabled;
  300. }
  301. //设置右侧按钮
  302. - (void)setRightButton:(UIButton *)rightButton {
  303. _titleView.rightButton = rightButton;
  304. }
  305. #pragma mark -
  306. #pragma mark 切换位置方法
  307. - (BOOL)switchToViewControllerAdIndex:(NSInteger)index animated:(BOOL)animated {
  308. if ([self numberOfPage] == 0) {return NO;}
  309. //如果正在加载中 返回
  310. if (self.pageVCAnimating && self.config.titleViewStyle == XLPageTitleViewStyleBasic) {return NO;}
  311. if (index == _selectedIndex && index >= 0 && self.pageVCDidLoad) {return NO;}
  312. //设置正在加载标记
  313. self.pageVCAnimating = animated;
  314. //更新当前位置
  315. _selectedIndex = index;
  316. //设置滚动方向
  317. UIPageViewControllerNavigationDirection direction = UIPageViewControllerNavigationDirectionForward;
  318. if (_titleView.lastSelectedIndex > _selectedIndex) {
  319. direction = UIPageViewControllerNavigationDirectionReverse;
  320. }
  321. //设置当前展示VC
  322. __weak typeof(self)weakSelf = self;
  323. [self.pageVC setViewControllers:@[[self viewControllerForIndex:index]] direction:direction animated:NO completion:^(BOOL finished) {
  324. weakSelf.pageVCAnimating = NO;
  325. }];
  326. //延时是为了避免切换视图时有其它操作阻塞UI
  327. self.view.userInteractionEnabled = NO;
  328. float delayTime = animated ? SetViewControllersMethodDelay : 0;
  329. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  330. weakSelf.view.userInteractionEnabled = YES;
  331. });
  332. //标题居中
  333. self.titleView.selectedIndex = _selectedIndex;
  334. return YES;
  335. }
  336. #pragma mark -
  337. #pragma mark 刷新方法
  338. - (void)reloadData {
  339. self.pageVCDidLoad = NO;
  340. //刷新标题栏UI
  341. [self.titleView reloadData];
  342. //刷新标题数据
  343. [self.allTitleArr removeAllObjects];
  344. for (NSInteger i = 0; i < [self numberOfPage]; i++) {
  345. [self.allTitleArr addObject:[self titleForIndex:i]];
  346. }
  347. }
  348. #pragma mark -
  349. #pragma mark 自定义方法
  350. - (void)registerClass:(Class)cellClass forTitleViewCellWithReuseIdentifier:(NSString *)identifier {
  351. [self.titleView registerClass:cellClass forTitleViewCellWithReuseIdentifier:identifier];
  352. }
  353. - (XLPageTitleCell *)dequeueReusableTitleViewCellWithIdentifier:(NSString *)identifier forIndex:(NSInteger)index {
  354. return [self.titleView dequeueReusableCellWithIdentifier:identifier forIndex:index];
  355. }
  356. #pragma mark -
  357. #pragma mark 辅助方法
  358. //指定位置的视图控制器 缓存方法
  359. - (UIViewController *)viewControllerForIndex:(NSInteger)index {
  360. //如果越界,则返回nil
  361. if (index < 0 || index >= [self numberOfPage]) {
  362. return nil;
  363. }
  364. //获取当前vc
  365. UIViewController *currentVC = self.pageVC.viewControllers.firstObject;
  366. //当前标题
  367. NSString *currentTitle = currentVC.xl_title;
  368. //目标切换位置标题
  369. NSString *targetTitle = [self titleForIndex:index];
  370. //如果和当前位置一样,则返回当前vc
  371. if ([currentTitle isEqualToString:targetTitle]) {
  372. return currentVC;
  373. }
  374. //如果之前显示过,则从内存中读取
  375. for (UIViewController *vc in self.shownVCArr) {
  376. if ([vc.xl_title isEqualToString:targetTitle]) {
  377. return vc;
  378. }
  379. }
  380. //如果之前没显示过,则通过dataSource创建
  381. UIViewController *vc = [self.dataSource pageViewController:self viewControllerForIndex:index];
  382. //设置扩展id,用户定位对应视图控制器
  383. vc.xl_title = [self titleForIndex:index];
  384. //设置试图控制器标题
  385. vc.title = [self titleForIndex:index];
  386. //把vc添加到缓存集合
  387. [self.shownVCArr addObject:vc];
  388. //添加子视图控制器
  389. [self addChildViewController:vc];
  390. return vc;
  391. }
  392. //指定位置的标题
  393. - (NSString *)titleForIndex:(NSInteger)index {
  394. return [self.dataSource pageViewController:self titleForIndex:index];
  395. }
  396. //总页数
  397. - (NSInteger)numberOfPage {
  398. return [self.dataSource pageViewControllerNumberOfPage];
  399. }
  400. //执行代理方法
  401. - (void)delegateSelectedAdIndex:(NSInteger)index {
  402. if (index == self.lastDelegateIndex) {return;}
  403. self.lastDelegateIndex = index;
  404. if ([self.delegate respondsToSelector:@selector(pageViewController:didSelectedAtIndex:)]) {
  405. [self.delegate pageViewController:self didSelectedAtIndex:index];
  406. }
  407. }
  408. @end